@metabase/cli 0.3.0 → 0.3.1-alpha.fix-transform-test-ee-path.a3ca6dd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{add-collection-B3Ty8SzB.mjs → add-collection-BEg-1pt8.mjs} +6 -9
- package/dist/{alert-DT-ee7yf.mjs → alert-CBwtJ3lE.mjs} +7 -7
- package/dist/{alerts-Bf2RH_p_.mjs → alerts-Bn9ZhFYK.mjs} +7 -7
- package/dist/{append-DFzQRPMd.mjs → append-F7kfyk0p.mjs} +6 -5
- package/dist/{archive-ByvA49Kz.mjs → archive-2VqBgcyE.mjs} +5 -5
- package/dist/{archive-4tmm946K.mjs → archive-3_KVbxIk.mjs} +4 -4
- package/dist/{archive-Cd29QC0c.mjs → archive-7tZyaUWv.mjs} +4 -4
- package/dist/{archive-Dr9Xnx_Z.mjs → archive-Bonv-Hcf.mjs} +6 -6
- package/dist/{archive-C7w49Grr.mjs → archive-BzEvapLz.mjs} +6 -6
- package/dist/{archive-CwAdlv63.mjs → archive-CAwDiQWt2.mjs} +6 -6
- package/dist/{archive-lQqs13Uz.mjs → archive-CE-RKcle.mjs} +6 -6
- package/dist/{archive-B6eXiyER.mjs → archive-Da3-hnRM.mjs} +4 -4
- package/dist/{archive-gid9Y09v.mjs → archive-IBHOtOMk.mjs} +4 -4
- package/dist/{archive-BxNPHxOY.mjs → archive-TBo05bZZ.mjs} +6 -6
- package/dist/{archive-ChORH5JH.mjs → archive-k79AXZhh.mjs} +4 -4
- package/dist/{auth-Dltxua89.mjs → auth-CLZuUSzL.mjs} +5 -5
- package/dist/{body-Cur4NJt9.mjs → body-Q8iNnt-X.mjs} +3 -3
- package/dist/{branches-DchlyU0i.mjs → branches-CjMIMKse.mjs} +4 -7
- package/dist/{cancel-BMQa5R1Z.mjs → cancel-H4vYr5Jv.mjs} +4 -4
- package/dist/{cancel-task-C5CU1pxU.mjs → cancel-task-p09LCbgs.mjs} +5 -8
- package/dist/{card-zyUkr2nS.mjs → card-CEkp1mcI.mjs} +1 -1
- package/dist/card-DlVb10D3.mjs +24 -0
- package/dist/{card-DZxRmver.mjs → card-voh1k4hl.mjs} +14 -11
- package/dist/{cards-SccWvRQD.mjs → cards-DDG6RObF.mjs} +7 -7
- package/dist/cli.mjs +567 -68
- package/dist/client-toDDqakD.mjs +4093 -0
- package/dist/{collection-CfulNRBG.mjs → collection-BxdPxD38.mjs} +1 -1
- package/dist/{collection-CGoVLvcD.mjs → collection-DYM0p3Xl.mjs} +1 -1
- package/dist/{collection-namespace-GwydWBF3.mjs → collection-namespace-DDI9-HgU.mjs} +2 -2
- package/dist/collection-p_sficxo.mjs +20 -0
- package/dist/content-translation-BrHOrmj9.mjs +5 -0
- package/dist/content-translation-DcTEH_d7.mjs +12 -0
- package/dist/{create-CAWytBE5.mjs → create-BLxdMJgQ.mjs} +5 -5
- package/dist/{create-BNQ_DytP.mjs → create-BPN1cX7b.mjs} +4 -4
- package/dist/{create-CHQx7jq4.mjs → create-BjCpsMQM.mjs} +5 -5
- package/dist/{create-CD008jdE.mjs → create-BovrhBQn.mjs} +12 -8
- package/dist/{create-DV-Ln5Mo.mjs → create-CVCPD-1F.mjs} +4 -4
- package/dist/{create-DJmXXQoq.mjs → create-CVPGOx7u2.mjs} +8 -8
- package/dist/{create-Beh-2uoN.mjs → create-Chs5c8vx.mjs} +6 -6
- package/dist/{create-BOLwWt8Q.mjs → create-D04fl6ec.mjs} +5 -5
- package/dist/{create-C79vErX0.mjs → create-D5mN20oE.mjs} +7 -7
- package/dist/{create-DF4QtgDL.mjs → create-D88Im3X22.mjs} +6 -6
- package/dist/{create-tQbnuz-I.mjs → create-DQfOV9Sm.mjs} +6 -6
- package/dist/{create-CCh_LyW9.mjs → create-DWU4j22P.mjs} +4 -4
- package/dist/create-DjP2yUyn.mjs +34 -0
- package/dist/{create-CAQV2g6V.mjs → create-Dk6Zm3ZH.mjs} +7 -7
- package/dist/{create-zeveFdnd.mjs → create-DkmeJGMy.mjs} +5 -8
- package/dist/{create-i14Wv3Tj.mjs → create-TQr3briq.mjs} +4 -4
- package/dist/{create-branch-CYo6jZth.mjs → create-branch-Bs6BlkgQ.mjs} +5 -9
- package/dist/{csv-B2Jgb1GM.mjs → csv-BsN6FmY-.mjs} +6 -5
- package/dist/{current-task-MpBFGttd.mjs → current-task-cCmLLVcv.mjs} +5 -8
- package/dist/{dashboard-BHJBoaG7.mjs → dashboard-CpIEKEgu.mjs} +10 -10
- package/dist/{dashboard-BCpSq_G-.mjs → dashboard-D0hwTC-f.mjs} +12 -2
- package/dist/{dashboard-CsIFFpzK.mjs → dashboard-DgK81azj.mjs} +1 -1
- package/dist/{database-B19qlLKZ.mjs → database-C3E9PZSx.mjs} +1 -1
- package/dist/{database-C8G6ZKG0.mjs → database-JtHTqSpq.mjs} +1 -1
- package/dist/{db-DsfojyeH.mjs → db-BbvcmfWM.mjs} +7 -7
- package/dist/{delete-B8e01xAG.mjs → delete-C4_rcfNu.mjs} +5 -5
- package/dist/delete-CLdDYdYG.mjs +44 -0
- package/dist/{delete-DFmX7jTp.mjs → delete-D13SLwhr.mjs} +5 -5
- package/dist/{delete-Ob06PPie.mjs → delete-D3-jMLu5.mjs} +5 -5
- package/dist/{delete-BApOjQvl.mjs → delete-DLirF5iq.mjs} +5 -5
- package/dist/{delete-Ck0q4Dzp.mjs → delete-JBjvxdIU.mjs} +5 -5
- package/dist/{delete-runtime-D74s6vhu.mjs → delete-runtime-BKEzJn22.mjs} +1 -2
- package/dist/{delete-table-DuyhGrvA.mjs → delete-table-Gwk7JL5c.mjs} +5 -5
- package/dist/{dependencies-Bj3QrGIm.mjs → dependencies-B7vl9Ovb.mjs} +7 -7
- package/dist/{dirty-DNYoM6Bp.mjs → dirty-CIZeASXG.mjs} +6 -9
- package/dist/{document-Cd2mmqCp.mjs → document-BHpLJbKU.mjs} +5 -0
- package/dist/{document-D6OybtP2.mjs → document-CA0Q6lqu.mjs} +1 -1
- package/dist/document-ceJW4VuL.mjs +19 -0
- package/dist/download-B-krJch0.mjs +25 -0
- package/dist/{eid-WTZmDdsr.mjs → eid-C0D92gZW.mjs} +4 -5
- package/dist/{events-Bn7jh5lP.mjs → events-CuLeaey6.mjs} +5 -5
- package/dist/{export-BjfoZRDJ.mjs → export-DeuLpt3x.mjs} +7 -10
- package/dist/{field-Zau6Hc6X.mjs → field-B8Hndt4p.mjs} +5 -5
- package/dist/{field-Cr2DPUG9.mjs → field-CcN4oZy6.mjs} +16 -1
- package/dist/{field-BPsNa-TO.mjs → field-fDtIrY6D.mjs} +1 -1
- package/dist/{fields-CBBa-Prp.mjs → fields-CAchmFh4.mjs} +7 -7
- package/dist/{get-CDJFVvhj.mjs → get-BH3Tgfgf.mjs} +4 -4
- package/dist/{get-CDMGbgrh.mjs → get-BJOxfbDP.mjs} +4 -4
- package/dist/{get-B_xdF1kq.mjs → get-BOjTqIMx.mjs} +6 -6
- package/dist/{get-D_h-G9bn.mjs → get-C1BPEbyl.mjs} +6 -6
- package/dist/{get-Dmj9Ygz1.mjs → get-C8dcHjd6.mjs} +6 -6
- package/dist/{get-DkWO0EI0.mjs → get-CArTALOt.mjs} +6 -6
- package/dist/get-CMCAgpxn.mjs +93 -0
- package/dist/{get-CnbUCzDM.mjs → get-CjDHpz6A.mjs} +4 -4
- package/dist/{get-ByBOq30r.mjs → get-ClNkea9I.mjs} +6 -6
- package/dist/{get-DRvsATp-.mjs → get-CvAMxfPD.mjs} +6 -6
- package/dist/{get-B7sMgsFs.mjs → get-D8qaSSEH.mjs} +4 -4
- package/dist/{get-00lyLv6R.mjs → get-DCvnNk3f.mjs} +6 -6
- package/dist/{get-DY03Azx9.mjs → get-DEwfm3Yl.mjs} +5 -8
- package/dist/{get-C2-Q3iQ5.mjs → get-DXWrGwWH.mjs} +4 -4
- package/dist/get-DbXTkB4u.mjs +32 -0
- package/dist/{get-DYO1kywc.mjs → get-DvF9oooq.mjs} +5 -5
- package/dist/{get-DdsoHaKH.mjs → get-d055CJ1T.mjs} +4 -4
- package/dist/{get-Bz99dAxK.mjs → get-kBTeBtLe.mjs} +6 -6
- package/dist/{get-DmpB6J9g.mjs → get-ohue3YrV2.mjs} +6 -6
- package/dist/{get-run-Cnux9ln0.mjs → get-run-X6zhp0dW.mjs} +6 -6
- package/dist/{get-Dn9ZiSZr.mjs → get-wIZrb7oN.mjs} +6 -6
- package/dist/git-sync-BkTmSgdD.mjs +28 -0
- package/dist/{git-sync-BDnxPrqU.mjs → git-sync-BwBRmEhE.mjs} +18 -1
- package/dist/{git-sync-DxD_ng48.mjs → git-sync-TjX1r_wF.mjs} +1 -1
- package/dist/{group-DBM87IgC.mjs → group-YQQ5GIcP.mjs} +2 -2
- package/dist/{has-remote-changes-C684Redh.mjs → has-remote-changes-zCtdEGs-.mjs} +5 -8
- package/dist/{import-Rmmttboo.mjs → import-DkAy_Xi8.mjs} +7 -10
- package/dist/{input-DH9i67x_.mjs → input-DEvkIjGr.mjs} +1 -1
- package/dist/{is-dirty-DYeJYhBz.mjs → is-dirty-Dfk2Xf-W.mjs} +3 -6
- package/dist/{items-CQlPXBXE.mjs → items-CiSBrc9d.mjs} +7 -7
- package/dist/{key-BEhqIzIQ.mjs → key-PYetuQ13.mjs} +1 -1
- package/dist/library-BPIzb3K_.mjs +81 -0
- package/dist/{library-Bsm5KsFF.mjs → library-FBj2Je-M.mjs} +5 -5
- package/dist/{library-DfYAT2Ep.mjs → library-PGCfm0UA.mjs} +1 -1
- package/dist/list-9CJczyF5.mjs +60 -0
- package/dist/{list-DE9FM957.mjs → list-B6tDzQsv.mjs} +4 -4
- package/dist/{list-BtkWNtvY.mjs → list-BDJkJc8s.mjs} +7 -7
- package/dist/{list-BzVTHOoH.mjs → list-Br0g0oDR.mjs} +6 -6
- package/dist/{list-CNUiF8Z7.mjs → list-C9-2J0Nu.mjs} +6 -6
- package/dist/{list-B4hJuMhA.mjs → list-CGjdrAhT.mjs} +7 -7
- package/dist/{list-BtmsNOEn.mjs → list-CHOJQ1uR.mjs} +4 -4
- package/dist/{list-CKK-s3Ty.mjs → list-CK4S_aDK.mjs} +4 -4
- package/dist/{list-C5Zs8nrb.mjs → list-CYONHM9n.mjs} +6 -6
- package/dist/{list-CQwOgIlj.mjs → list-CrwbvMEG.mjs} +6 -6
- package/dist/{list-BOET6zhP.mjs → list-D6WZmnG_.mjs} +4 -4
- package/dist/{list-DMKDsBRd.mjs → list-DD_tjCmP.mjs} +7 -7
- package/dist/{list-DF2rvoO1.mjs → list-Dqv3nOEe.mjs} +14 -13
- package/dist/{list-CQDprvW0.mjs → list-Hjsg2N8L.mjs} +7 -7
- package/dist/{list-CMpoc2TN.mjs → list-M4Twal0d.mjs} +5 -5
- package/dist/{list-BpFp9Wre.mjs → list-UbzhJFFn.mjs} +4 -4
- package/dist/list-W5y4079U.mjs +35 -0
- package/dist/{list-BP3MPq-M.mjs → list-f5HIVoLN.mjs} +4 -4
- package/dist/{list-B8nAGwP4.mjs → list-uCAP4vmQ.mjs} +6 -6
- package/dist/{login-CGHeSjlP.mjs → login-CIzUMkf3.mjs} +15 -12
- package/dist/{logout-DAxOlbv4.mjs → logout-CRm_AaoF.mjs} +3 -4
- package/dist/{measure-BsYv0p26.mjs → measure-DPXeIN18.mjs} +6 -6
- package/dist/{notification-Fcq1fIPw.mjs → notification-DVrLkjFT.mjs} +1 -1
- package/dist/{parameter-DUKR8NXo.mjs → parameter-Celg0wZa.mjs} +1 -1
- package/dist/{parameter-values-CJTdqd8c.mjs → parameter-values-rOHoy3k0.mjs} +5 -6
- package/dist/{parse-enum-D0Jfm_Bz.mjs → parse-enum-Dc0dMZ3P.mjs} +1 -1
- package/dist/{parse-id-C_jmKFaa.mjs → parse-id-CYZAIB58.mjs} +1 -1
- package/dist/{parse-ref-ibKVB0J4.mjs → parse-ref-DTDld_LJ.mjs} +1 -1
- package/dist/{path-B5gHvrYL.mjs → path-D9_unWc1.mjs} +5 -5
- package/dist/{poll-75SvT7Zv.mjs → poll-CZeXepUQ.mjs} +2 -2
- package/dist/{preflight-YpPQo-sa.mjs → preflight-BIxRV-bm.mjs} +3 -4
- package/dist/{process-DBOOrmET.mjs → process-CTrfxxaC.mjs} +1 -1
- package/dist/{publish-DijrxaCW.mjs → publish-N509vd0U.mjs} +9 -9
- package/dist/{pulse-BmsDenXm.mjs → pulse-Dz0ocv0l.mjs} +1 -1
- package/dist/{pulse-DhG6I9mI.mjs → pulse-x0gFxWfG.mjs} +2 -2
- package/dist/{query-Dc__-KJH.mjs → query-JvMjF22L.mjs} +9 -10
- package/dist/{query-result-DBDwk8TS.mjs → query-result-E4Q-Gra6.mjs} +1 -1
- package/dist/{query-BbqECzmF.mjs → query-yh5Wkfmc.mjs} +11 -18
- package/dist/{remove-collection-BOnxYvhj.mjs → remove-collection-B0E0dlZt.mjs} +6 -9
- package/dist/{render-CI93iP0I.mjs → render-Tlz6OCMU.mjs} +13 -2
- package/dist/{replace-Cq4CxVa-.mjs → replace-BwbYo6-2.mjs} +6 -5
- package/dist/{rescan-values-Dr_j416g.mjs → rescan-values-BiMPb5Z5.mjs} +6 -6
- package/dist/{run-DdGJ544X.mjs → run-B9aA1aNI.mjs} +26 -14
- package/dist/run-D37ntUQo.mjs +50 -0
- package/dist/{run-C3DkS6NP.mjs → run-Dbh3nvzF.mjs} +6 -6
- package/dist/{runs-CUNWiidL.mjs → runs-BuUzKR-x.mjs} +7 -7
- package/dist/{runtime-DiKyBxOw.mjs → runtime-CwhhhwcB.mjs} +946 -246
- package/dist/{schema-tables-CV386fnC.mjs → schema-tables-9PvvygPu.mjs} +7 -7
- package/dist/{schemas-DM-UqZBQ.mjs → schemas-DqkQI39t.mjs} +5 -5
- package/dist/{search-DFBwUc6d.mjs → search-BAXg-EJw.mjs} +2 -1
- package/dist/{search-DmGScXs8.mjs → search-BjAwR-oP.mjs} +6 -6
- package/dist/{segment-BzUGM2Fr.mjs → segment-1nBcugv8.mjs} +6 -6
- package/dist/{selectors-DGDn0Hto.mjs → selectors-DFNdyhPn.mjs} +3 -3
- package/dist/{send-CVxxEpDB.mjs → send-D4tYN3aN.mjs} +4 -4
- package/dist/{set-DFolTjbd.mjs → set-D_Mh2eVO.mjs} +5 -5
- package/dist/{set-active-BD8L1lPX.mjs → set-active-jY5VZsO8.mjs} +5 -5
- package/dist/{setting-DyhaeICs.mjs → setting-9ULb5wjN.mjs} +4 -4
- package/dist/{setup-BbDQN8fe.mjs → setup-DMaIuuCV.mjs} +4 -4
- package/dist/{signal-CMd0EFUa.mjs → signal-k7cm5N6h.mjs} +11 -2
- package/dist/skill-list-Bucy7UE2.mjs +76 -0
- package/dist/{skills--vK6J-e4.mjs → skills-Dyfqgobg.mjs} +3 -3
- package/dist/skills-PjjRtYqi.mjs +351 -0
- package/dist/snippet-DNoTIGAs.mjs +19 -0
- package/dist/{stash-D0S4JmAz.mjs → stash-C6BEM6_u.mjs} +7 -11
- package/dist/{status-BhswxJL1.mjs → status-DiP093Vm.mjs} +11 -9
- package/dist/{status-hL1am-HV.mjs → status-wAg5ajzM.mjs} +12 -11
- package/dist/stream-McixcoUl.mjs +7 -0
- package/dist/{subscription-_d5fB45F.mjs → subscription-CEeI0pSr.mjs} +6 -6
- package/dist/{subscriptions-s4TF6Ax3.mjs → subscriptions-B9cMNuIS.mjs} +7 -7
- package/dist/{summary-CsPvcUwW.mjs → summary-D0NP2NoT.mjs} +6 -6
- package/dist/{sync-schema-DaZ_MV-u.mjs → sync-schema-BgJ6KEqa.mjs} +7 -7
- package/dist/{sync-task-BACcrs0i.mjs → sync-task-BXeWuARG.mjs} +1 -1
- package/dist/{table-DYoC1kPn.mjs → table-9bTG6zSa.mjs} +5 -5
- package/dist/{table-CsIJNPFf.mjs → table-C6h4tIRw.mjs} +1 -1
- package/dist/{table-CeeTJgQb.mjs → table-DfuY0N8e.mjs} +51 -4
- package/dist/timeline-Ck53UgDh.mjs +21 -0
- package/dist/{timeline-event-CNQdCTDg.mjs → timeline-event-POmQ2TD2.mjs} +6 -6
- package/dist/transform-BC39AgQF.mjs +28 -0
- package/dist/{transform-DoTyyXAu.mjs → transform-CtqB1WYo.mjs} +2 -2
- package/dist/{transform-D3QX0jxA.mjs → transform-DiW7bREn.mjs} +68 -5
- package/dist/{transform-job-50GZHhKH.mjs → transform-job-AaMZRxAq.mjs} +1 -1
- package/dist/{transform-job-DRrK28IH.mjs → transform-job-CCguL6LP.mjs} +41 -4
- package/dist/transform-job-D-zUFmZU.mjs +22 -0
- package/dist/{transform-tag-_tgwlsdB.mjs → transform-tag-CP7FRU5G.mjs} +5 -5
- package/dist/transform-test-BjjCQ2xm.mjs +169 -0
- package/dist/transform-test-CKNQS_kC.mjs +16 -0
- package/dist/transform-test-gzDcbypy.mjs +25 -0
- package/dist/{transforms-oU4zizbO.mjs → transforms-DDSNP4eL.mjs} +7 -7
- package/dist/{tree-CH-f3bjP.mjs → tree-CAnMS2dm.mjs} +4 -5
- package/dist/{unpublish-BUl9RU5C.mjs → unpublish-DF1XQMWJ.mjs} +4 -7
- package/dist/{update-BZPllQoR.mjs → update-BS1yiwOO.mjs} +5 -5
- package/dist/{update-CO4m4wH3.mjs → update-Bb89y-0M.mjs} +8 -8
- package/dist/{update-C4YJQFDS.mjs → update-BbiVvdm1.mjs} +7 -7
- package/dist/{update-BCS5FxKr.mjs → update-BcdLQu7-.mjs} +9 -9
- package/dist/{update-cYkvbqK8.mjs → update-BjcWwrPc.mjs} +5 -5
- package/dist/{update-tX9mKBN_.mjs → update-BoJJ6Gel.mjs} +9 -9
- package/dist/{update-CHNSUHK4.mjs → update-C09GGTjN.mjs} +5 -5
- package/dist/{update-BzMDeZiU.mjs → update-C33R41eM.mjs} +7 -7
- package/dist/{update-CmDTljK-.mjs → update-C6KQZMDF.mjs} +7 -7
- package/dist/{update-bD_Jh5-s.mjs → update-CFqdElFO.mjs} +6 -6
- package/dist/{update-CVPAwJaH.mjs → update-CHsXoUJQ.mjs} +6 -6
- package/dist/update-CaJO1rWk.mjs +41 -0
- package/dist/{update-CvOO8TED.mjs → update-CmXGZPCR.mjs} +7 -7
- package/dist/{update-D-NE60AV.mjs → update-DFJSD3_l.mjs} +7 -7
- package/dist/{update-Nfi44RrE.mjs → update-DOeBAoul.mjs} +6 -6
- package/dist/{update-BiFDBcEH.mjs → update-DiowA7ud.mjs} +5 -5
- package/dist/{update-dashcard-C3TuYO_k.mjs → update-dashcard-DSL6Bogg.mjs} +7 -7
- package/dist/{upgrade-DXMV4AyP.mjs → upgrade-CBxr91bH.mjs} +6 -8
- package/dist/upload-C3z5ZaXA.mjs +24 -0
- package/dist/upload-CSHcnonI.mjs +13 -0
- package/dist/upload-DTyF-p8X.mjs +39 -0
- package/dist/upload-Do2PBSeH.mjs +23 -0
- package/dist/{uuid-BJkDWj9p.mjs → uuid-CwEbjDp8.mjs} +3 -4
- package/dist/{validate-BWchyW0V.mjs → validate-EAB-1EkK.mjs} +1 -1
- package/dist/{validate-query-DaGYSmpQ.mjs → validate-query-CQG86147.mjs} +2 -3
- package/dist/{values-Dxf1JEoA.mjs → values-69IZEVXu.mjs} +6 -6
- package/dist/{verify-K96rItG5.mjs → verify-Dg5YBI0l.mjs} +3 -18
- package/dist/{wait-DoMDH226.mjs → wait-DVXEJXd3.mjs} +7 -10
- package/dist/{wait-flags-DWEF9tX4.mjs → wait-flags-CkRgnfww.mjs} +2 -2
- package/dist/{window-BkvrP0gB.mjs → window-DSb1tqSe.mjs} +1 -1
- package/package.json +1 -1
- package/skill-data/core/SKILL.md +31 -14
- package/skill-data/dashboard/SKILL.md +2 -2
- package/skill-data/data-workflow/references/building-clean-tables.md +6 -2
- package/skill-data/data-workflow/references/reusable-definitions.md +11 -3
- package/skill-data/document/SKILL.md +4 -1
- package/skill-data/git-sync/SKILL.md +5 -0
- package/skill-data/mbql/SKILL.md +1 -1
- package/skill-data/mbql/references/operators.md +20 -15
- package/skill-data/metadata/SKILL.md +1 -1
- package/skill-data/metadata/references/semantic-types.md +7 -1
- package/skill-data/native-sql/references/template-tags.md +6 -2
- package/skill-data/notification/SKILL.md +7 -1
- package/skill-data/transform/SKILL.md +75 -3
- package/skill-data/transform-test-plan/SKILL.md +171 -0
- package/skill-data/transform-test-plan/references/checklist.md +81 -0
- package/skill-data/transform-test-plan/references/checks.md +259 -0
- package/skill-data/visualization/SKILL.md +11 -3
- package/skill-data/visualization/references/settings.md +6 -4
- package/skills/metabase-cli/SKILL.md +1 -1
- package/dist/card-CHI2bmds.mjs +0 -24
- package/dist/client-BqhzKdLM.mjs +0 -2194
- package/dist/collection-CY0AB99E.mjs +0 -20
- package/dist/document-BbskZPTT.mjs +0 -19
- package/dist/get-CvI--9S_.mjs +0 -80
- package/dist/git-sync-MQQYRYLg.mjs +0 -28
- package/dist/library-ClLgrAXy.mjs +0 -19
- package/dist/list-UhpKG6_m.mjs +0 -98
- package/dist/network-error-CEb9ymDW.mjs +0 -423
- package/dist/predicates-Bkm2IoeX.mjs +0 -170
- package/dist/skills-DTCrMcix.mjs +0 -216
- package/dist/snippet-DRwKb2x7.mjs +0 -19
- package/dist/timeline-xlDbEKWg.mjs +0 -21
- package/dist/transform-Bs0EWijQ.mjs +0 -28
- package/dist/transform-job-BcLChRup.mjs +0 -22
- package/dist/upload-CH632qW6.mjs +0 -46
- package/dist/upload-Dey_8rLf.mjs +0 -13
|
@@ -0,0 +1,4093 @@
|
|
|
1
|
+
import { $ as InternalError, Q as ConfigError, at as ValidationError, n as methodRequirements, nt as ResponseShapeError, ot as errorMessage, rt as TimeoutError, tt as NetworkError } from "./cli.mjs";
|
|
2
|
+
import { n as Notification, r as NotificationCardPayload, t as CARD_PAYLOAD_TYPE } from "./notification-DaA4bBHW.mjs";
|
|
3
|
+
import { $ as createServerProfile, B as JSON_CONTENT_TYPE, F as buildNetworkError, G as credentialSecrets, H as parseJsonOrPlain, I as isConnectionClosed, J as normalizeUrl, L as HttpError, U as assertCredentialHeaderSafe, V as parseJson, W as credentialAuthHeader, Z as checkFeatures, et as probeServer, tt as CapabilityError, z as isRetryableStatus } from "./runtime-CwhhhwcB.mjs";
|
|
4
|
+
import { n as throwIfAborted, r as untilAborted, t as combineAborts } from "./signal-k7cm5N6h.mjs";
|
|
5
|
+
import { a as FieldSearchMatches, i as FieldRemappedValue, l as FieldValues, n as FieldBaseType, o as FieldSemanticType, t as Field } from "./field-CcN4oZy6.mjs";
|
|
6
|
+
import { a as TemporalUnit, r as ParameterValues } from "./parameter-Celg0wZa.mjs";
|
|
7
|
+
import { a as CardQueryResult, l as DatasetQuery, s as CardType, t as Card } from "./card-voh1k4hl.mjs";
|
|
8
|
+
import { c as TableVisibilityType, i as TableForeignKey, o as TableQueryMetadata, r as TableDataLayerTier, t as Table } from "./table-DfuY0N8e.mjs";
|
|
9
|
+
import { t as Database } from "./database-JtHTqSpq.mjs";
|
|
10
|
+
import { t as Snippet } from "./snippet-CyKAgIZl.mjs";
|
|
11
|
+
import { c as CollectionItem, i as CollectionAuthorityLevel, m as CollectionTreeNode, r as Collection, s as CollectionId } from "./collection-BxdPxD38.mjs";
|
|
12
|
+
import { t as ContentTranslationUploadResult } from "./content-translation-BrHOrmj9.mjs";
|
|
13
|
+
import { a as DashboardDetail, c as Dashcard, r as DashboardCopy, t as Dashboard } from "./dashboard-D0hwTC-f.mjs";
|
|
14
|
+
import { r as pollUntil } from "./poll-CZeXepUQ.mjs";
|
|
15
|
+
import { t as Document } from "./document-BHpLJbKU.mjs";
|
|
16
|
+
import { i as EidTranslateResult } from "./eid-translation-zEHtr5Oc.mjs";
|
|
17
|
+
import { c as SyncSettingsUpdateResult, i as SyncExportPreflight, n as SyncDirtyItem, p as isSyncTaskTerminal, s as SyncRemoteChanges, t as SyncBranchCreated, u as SyncTask } from "./git-sync-BwBRmEhE.mjs";
|
|
18
|
+
import { a as toLibrary, i as libraryWireSchema, n as LibraryCollectionInfo } from "./library-BPIzb3K_.mjs";
|
|
19
|
+
import { t as Measure } from "./measure-CtfjqJFP.mjs";
|
|
20
|
+
import { t as Pulse } from "./pulse-Dz0ocv0l.mjs";
|
|
21
|
+
import { a as TransformDagTransform, d as TransformTarget, g as transformRowSchema, h as transformDetailSchema, i as TransformDagRunResult, l as TransformRunSummary, m as isTransformRunTerminal, o as TransformRun, p as isTransformRunFailed, u as TransformSourceTableEntry } from "./transform-DiW7bREn.mjs";
|
|
22
|
+
import { r as SearchResult } from "./search-BAXg-EJw.mjs";
|
|
23
|
+
import { t as Segment } from "./segment-OsrbUDzw.mjs";
|
|
24
|
+
import { t as Setting } from "./setting-DTiLCW8g.mjs";
|
|
25
|
+
import { n as SetupResult } from "./setup-CO7QV-ao.mjs";
|
|
26
|
+
import { i as TimelineEvent, t as Timeline } from "./timeline-CCSU2qOS.mjs";
|
|
27
|
+
import { c as transformJobSchema, n as TransformJobActiveResult, s as transformJobRunResultSchema } from "./transform-job-CCguL6LP.mjs";
|
|
28
|
+
import { t as TransformTag } from "./transform-tag-CcElGp3C.mjs";
|
|
29
|
+
import { i as TransformTestRunResult, t as TransformTest } from "./transform-test-BjjCQ2xm.mjs";
|
|
30
|
+
import { t as UploadResult } from "./upload-DAtxBBms.mjs";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import { setTimeout } from "node:timers/promises";
|
|
33
|
+
//#region \0rolldown/runtime.js
|
|
34
|
+
var __defProp = Object.defineProperty;
|
|
35
|
+
var __exportAll = (all, no_symbols) => {
|
|
36
|
+
let target = {};
|
|
37
|
+
for (var name in all) __defProp(target, name, {
|
|
38
|
+
get: all[name],
|
|
39
|
+
enumerable: true
|
|
40
|
+
});
|
|
41
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
42
|
+
return target;
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region ../client/src/http/response-shape.ts
|
|
46
|
+
async function parseJsonResponse(text, schema, context) {
|
|
47
|
+
try {
|
|
48
|
+
return parseJson(text, schema, { source: context.url });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof ValidationError) throw ResponseShapeError.fromZodIssues({
|
|
51
|
+
kind: "zod",
|
|
52
|
+
method: context.method,
|
|
53
|
+
url: context.url,
|
|
54
|
+
status: context.status,
|
|
55
|
+
zodIssues: error.developerDetail.zodIssues,
|
|
56
|
+
serverTag: await context.getServerTag(),
|
|
57
|
+
serverSkew: context.serverSkew
|
|
58
|
+
});
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region ../client/src/http/retry.ts
|
|
64
|
+
const BASE_DELAY_MS = 250;
|
|
65
|
+
const MAX_DELAY_MS = 8e3;
|
|
66
|
+
const MS_PER_SECOND = 1e3;
|
|
67
|
+
function backoffDelay({ attempt, retryAfterHeader }) {
|
|
68
|
+
const fromHeader = parseRetryAfter(retryAfterHeader);
|
|
69
|
+
if (fromHeader !== null) return fromHeader;
|
|
70
|
+
return Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
|
|
71
|
+
}
|
|
72
|
+
function parseRetryAfter(header) {
|
|
73
|
+
if (!header) return null;
|
|
74
|
+
const numeric = Number(header);
|
|
75
|
+
if (Number.isFinite(numeric) && numeric >= 0) return Math.round(numeric * MS_PER_SECOND);
|
|
76
|
+
const dateMs = Date.parse(header);
|
|
77
|
+
if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now());
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function sleep(ms, signal) {
|
|
81
|
+
return setTimeout(ms, void 0, { signal });
|
|
82
|
+
}
|
|
83
|
+
async function runWithRetries(attempt, signal) {
|
|
84
|
+
let attemptIndex = 0;
|
|
85
|
+
while (true) {
|
|
86
|
+
const outcome = await attempt(attemptIndex);
|
|
87
|
+
if (outcome.kind === "success") return outcome.response;
|
|
88
|
+
await sleep(outcome.delayMs, signal);
|
|
89
|
+
attemptIndex += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region ../client/src/http/transport.ts
|
|
94
|
+
const UNAUTHORIZED_STATUS = 401;
|
|
95
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
96
|
+
const OCTET_STREAM_CONTENT_TYPE = "application/octet-stream";
|
|
97
|
+
const TEXT_CONTENT_TYPE_PREFIX = "text/";
|
|
98
|
+
const ERROR_BODY_BYTE_CAP = 64 * 1024;
|
|
99
|
+
const IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
|
|
100
|
+
"GET",
|
|
101
|
+
"HEAD",
|
|
102
|
+
"OPTIONS"
|
|
103
|
+
]);
|
|
104
|
+
function createTransport(config, options) {
|
|
105
|
+
const baseUrl = normalizeUrl(config.url);
|
|
106
|
+
assertCredentialHeaderSafe(config.credential);
|
|
107
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
108
|
+
let settledProfile = options.server ?? null;
|
|
109
|
+
const getServerTag = options.getServerTag ?? (async () => tagOf(settledProfile));
|
|
110
|
+
const serverSkew = () => settledProfile === null ? null : settledProfile.skew;
|
|
111
|
+
const refreshCredential = options.refreshCredential;
|
|
112
|
+
let credential = config.credential;
|
|
113
|
+
const knownSecrets = new Set(credentialSecrets(credential));
|
|
114
|
+
const redactionContext = { knownSecrets };
|
|
115
|
+
let refreshInFlight = null;
|
|
116
|
+
function tryRefreshCredential() {
|
|
117
|
+
const refresh = refreshCredential;
|
|
118
|
+
if (credential.kind !== "oauth" || refresh === void 0) return Promise.resolve(false);
|
|
119
|
+
refreshInFlight ??= refreshOnce(refresh).finally(() => {
|
|
120
|
+
refreshInFlight = null;
|
|
121
|
+
});
|
|
122
|
+
return refreshInFlight;
|
|
123
|
+
}
|
|
124
|
+
async function refreshOnce(refresh) {
|
|
125
|
+
const next = await refresh();
|
|
126
|
+
if (next === null) return false;
|
|
127
|
+
credential = next;
|
|
128
|
+
for (const secret of credentialSecrets(next)) knownSecrets.add(secret);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
async function attemptOnce(prepared, attempt) {
|
|
132
|
+
const hasRetriesLeft = attempt < prepared.retries;
|
|
133
|
+
const timeoutSignal = AbortSignal.timeout(prepared.timeoutMs);
|
|
134
|
+
const combined = combineAborts(timeoutSignal, prepared.cancelSignal);
|
|
135
|
+
let response;
|
|
136
|
+
try {
|
|
137
|
+
response = await fetchImpl(prepared.url, {
|
|
138
|
+
method: prepared.method,
|
|
139
|
+
headers: prepared.headers,
|
|
140
|
+
body: prepared.body,
|
|
141
|
+
signal: combined
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throwIfAborted(prepared.cancelSignal);
|
|
145
|
+
const isStaleSocketFirstTry = attempt === 0 && isConnectionClosed(error);
|
|
146
|
+
if (prepared.idempotent && (hasRetriesLeft || isStaleSocketFirstTry)) return {
|
|
147
|
+
kind: "retry",
|
|
148
|
+
delayMs: backoffDelay({ attempt })
|
|
149
|
+
};
|
|
150
|
+
if (timeoutSignal.aborted) throw new TimeoutError(`Request timed out after ${prepared.timeoutMs}ms`, {
|
|
151
|
+
kind: "http",
|
|
152
|
+
method: prepared.method,
|
|
153
|
+
url: prepared.url,
|
|
154
|
+
timeoutMs: prepared.timeoutMs
|
|
155
|
+
});
|
|
156
|
+
throw buildNetworkError(error, prepared.method, prepared.url);
|
|
157
|
+
}
|
|
158
|
+
const canRetryStatus = hasRetriesLeft && prepared.idempotent;
|
|
159
|
+
if (!response.ok && isRetryableStatus(response.status) && canRetryStatus) {
|
|
160
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
161
|
+
response.body?.cancel().catch(() => void 0);
|
|
162
|
+
return {
|
|
163
|
+
kind: "retry",
|
|
164
|
+
delayMs: backoffDelay({
|
|
165
|
+
attempt,
|
|
166
|
+
retryAfterHeader: retryAfter
|
|
167
|
+
})
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
const rawBody = await readBodyForError(response);
|
|
172
|
+
const serverTag = await getServerTag();
|
|
173
|
+
throw new HttpError({
|
|
174
|
+
status: response.status,
|
|
175
|
+
statusText: response.statusText,
|
|
176
|
+
method: prepared.method,
|
|
177
|
+
url: prepared.url,
|
|
178
|
+
responseHeaders: response.headers,
|
|
179
|
+
rawBody,
|
|
180
|
+
serverTag,
|
|
181
|
+
redactionContext
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
assertContentType(response, prepared);
|
|
185
|
+
return {
|
|
186
|
+
kind: "success",
|
|
187
|
+
response
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function executeRaw(prepared) {
|
|
191
|
+
try {
|
|
192
|
+
return await runWithRetries((attempt) => attemptOnce(prepared, attempt), prepared.cancelSignal);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throwIfAborted(prepared.cancelSignal);
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function executeWithAuthRefresh(path, opts) {
|
|
199
|
+
const prepared = prepare(path, opts);
|
|
200
|
+
try {
|
|
201
|
+
return {
|
|
202
|
+
response: await executeRaw(prepared),
|
|
203
|
+
prepared
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error instanceof HttpError && error.status === UNAUTHORIZED_STATUS) {
|
|
207
|
+
if (await tryRefreshCredential()) {
|
|
208
|
+
const retried = prepare(path, opts);
|
|
209
|
+
return {
|
|
210
|
+
response: await executeRaw(retried),
|
|
211
|
+
prepared: retried
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function prepare(path, opts = {}) {
|
|
219
|
+
const method = opts.method ?? "GET";
|
|
220
|
+
const expectContentType = opts.expectContentType ?? "json";
|
|
221
|
+
const url = buildUrl(baseUrl, path, opts.query);
|
|
222
|
+
const headers = new Headers();
|
|
223
|
+
const auth = credentialAuthHeader(credential);
|
|
224
|
+
headers.set(auth.name, auth.value);
|
|
225
|
+
headers.set("accept", acceptHeader(expectContentType));
|
|
226
|
+
headers.set("user-agent", options.userAgent);
|
|
227
|
+
let body = null;
|
|
228
|
+
if (opts.body !== void 0 && opts.body !== null) if (typeof opts.body === "string" || opts.body instanceof URLSearchParams) body = opts.body;
|
|
229
|
+
else if (opts.body instanceof FormData || opts.body instanceof ReadableStream) body = opts.body;
|
|
230
|
+
else if (opts.body instanceof Uint8Array) {
|
|
231
|
+
body = opts.body;
|
|
232
|
+
headers.set("content-type", OCTET_STREAM_CONTENT_TYPE);
|
|
233
|
+
} else {
|
|
234
|
+
body = JSON.stringify(opts.body);
|
|
235
|
+
headers.set("content-type", JSON_CONTENT_TYPE);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
url,
|
|
239
|
+
method,
|
|
240
|
+
headers,
|
|
241
|
+
body,
|
|
242
|
+
expectContentType,
|
|
243
|
+
retries: opts.retries ?? 3,
|
|
244
|
+
idempotent: opts.idempotent ?? IDEMPOTENT_METHODS.has(method),
|
|
245
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
246
|
+
cancelSignal: combineAborts(opts.signal, options.signal)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
let serverProfile = options.server === void 0 ? null : Promise.resolve(options.server);
|
|
250
|
+
async function server(wait = {}) {
|
|
251
|
+
throwIfAborted(wait.signal);
|
|
252
|
+
if (serverProfile === null) {
|
|
253
|
+
const probe = probeServer(transport).then((info) => {
|
|
254
|
+
settledProfile = createServerProfile(info);
|
|
255
|
+
return settledProfile;
|
|
256
|
+
});
|
|
257
|
+
probe.catch(() => {
|
|
258
|
+
serverProfile = null;
|
|
259
|
+
});
|
|
260
|
+
serverProfile = probe;
|
|
261
|
+
}
|
|
262
|
+
return untilAborted(serverProfile, wait.signal);
|
|
263
|
+
}
|
|
264
|
+
const enforceRequirements = options.enforceRequirements ?? true;
|
|
265
|
+
async function requireFeatures(features, wait = {}) {
|
|
266
|
+
if (!enforceRequirements || features.length === 0) return;
|
|
267
|
+
const failure = checkFeatures(features, await server(wait));
|
|
268
|
+
if (failure !== null) throw new CapabilityError(failure);
|
|
269
|
+
}
|
|
270
|
+
async function requireMethod(key, wait = {}) {
|
|
271
|
+
return requireFeatures(methodRequirements(key), wait);
|
|
272
|
+
}
|
|
273
|
+
const transport = {
|
|
274
|
+
server,
|
|
275
|
+
require: requireMethod,
|
|
276
|
+
requireFeatures,
|
|
277
|
+
async requestRaw(path, opts) {
|
|
278
|
+
return (await executeWithAuthRefresh(path, opts ?? {})).response;
|
|
279
|
+
},
|
|
280
|
+
async requestParsed(schema, path, opts) {
|
|
281
|
+
const { response, prepared } = await executeWithAuthRefresh(path, {
|
|
282
|
+
...opts,
|
|
283
|
+
expectContentType: "json"
|
|
284
|
+
});
|
|
285
|
+
return parseJsonResponse(await response.text(), schema, {
|
|
286
|
+
method: prepared.method,
|
|
287
|
+
url: prepared.url,
|
|
288
|
+
status: response.status,
|
|
289
|
+
getServerTag,
|
|
290
|
+
serverSkew: serverSkew()
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
async requestStream(path, opts) {
|
|
294
|
+
const { response, prepared } = await executeWithAuthRefresh(path, {
|
|
295
|
+
...opts,
|
|
296
|
+
expectContentType: opts?.expectContentType ?? "binary"
|
|
297
|
+
});
|
|
298
|
+
if (!response.body) throw new NetworkError("Response had no body to stream", {
|
|
299
|
+
method: prepared.method,
|
|
300
|
+
url: prepared.url,
|
|
301
|
+
cause: "missing body"
|
|
302
|
+
});
|
|
303
|
+
return response.body;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
return transport;
|
|
307
|
+
}
|
|
308
|
+
function tagOf(profile) {
|
|
309
|
+
if (profile === null || profile.version === null) return null;
|
|
310
|
+
return profile.version.tag;
|
|
311
|
+
}
|
|
312
|
+
function buildUrl(baseUrl, path, query) {
|
|
313
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
314
|
+
const target = new URL(baseUrl + normalizedPath);
|
|
315
|
+
if (query) for (const [key, value] of Object.entries(query)) {
|
|
316
|
+
if (value === void 0) continue;
|
|
317
|
+
if (Array.isArray(value)) for (const entry of value) target.searchParams.append(key, String(entry));
|
|
318
|
+
else target.searchParams.append(key, String(value));
|
|
319
|
+
}
|
|
320
|
+
return target.toString();
|
|
321
|
+
}
|
|
322
|
+
function acceptHeader(expected) {
|
|
323
|
+
if (expected === "json") return JSON_CONTENT_TYPE;
|
|
324
|
+
if (expected === "text") return "text/*";
|
|
325
|
+
return "*/*";
|
|
326
|
+
}
|
|
327
|
+
function assertContentType(response, prepared) {
|
|
328
|
+
if (prepared.expectContentType === "binary") return;
|
|
329
|
+
const contentType = response.headers.get("content-type");
|
|
330
|
+
if (contentType === null) throwContentTypeMismatch(response, prepared, prepared.expectContentType);
|
|
331
|
+
if (prepared.expectContentType === "json" && !contentType.includes("application/json")) throwContentTypeMismatch(response, prepared, "json");
|
|
332
|
+
if (prepared.expectContentType === "text" && !contentType.startsWith(TEXT_CONTENT_TYPE_PREFIX)) throwContentTypeMismatch(response, prepared, "text");
|
|
333
|
+
}
|
|
334
|
+
function throwContentTypeMismatch(response, prepared, expected) {
|
|
335
|
+
const actual = response.headers.get("content-type") ?? "no content-type";
|
|
336
|
+
throw new HttpError({
|
|
337
|
+
status: response.status,
|
|
338
|
+
statusText: response.statusText,
|
|
339
|
+
method: prepared.method,
|
|
340
|
+
url: prepared.url,
|
|
341
|
+
responseHeaders: response.headers,
|
|
342
|
+
rawBody: null,
|
|
343
|
+
overrideUserMessage: `Expected ${expected} response but got ${actual}`
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
async function readBodyForError(response) {
|
|
347
|
+
try {
|
|
348
|
+
return Buffer.from(await response.arrayBuffer()).subarray(0, ERROR_BODY_BYTE_CAP).toString("utf8");
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return `[body read failed: ${errorMessage(error)}]`;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region ../client/src/domain/dataset.ts
|
|
355
|
+
const CompiledQuery = z.object({
|
|
356
|
+
query: z.union([z.string(), z.array(z.unknown())]),
|
|
357
|
+
params: z.array(z.unknown()).nullable().optional(),
|
|
358
|
+
collection: z.string().optional()
|
|
359
|
+
}).loose();
|
|
360
|
+
const FieldRef = z.tuple([
|
|
361
|
+
z.literal("field"),
|
|
362
|
+
z.string(),
|
|
363
|
+
z.object({ "base-type": FieldBaseType }).loose()
|
|
364
|
+
]);
|
|
365
|
+
const VirtualField = z.object({
|
|
366
|
+
id: z.union([z.number().int(), FieldRef]),
|
|
367
|
+
table_id: z.string(),
|
|
368
|
+
name: z.string(),
|
|
369
|
+
display_name: z.string(),
|
|
370
|
+
base_type: FieldBaseType,
|
|
371
|
+
semantic_type: FieldSemanticType.nullable(),
|
|
372
|
+
fk_target_field_id: z.number().int().nullable().optional()
|
|
373
|
+
}).loose();
|
|
374
|
+
const VirtualTable = z.object({
|
|
375
|
+
id: z.string(),
|
|
376
|
+
db_id: z.number().int(),
|
|
377
|
+
display_name: z.string(),
|
|
378
|
+
schema: z.string(),
|
|
379
|
+
description: z.string().nullable(),
|
|
380
|
+
type: CardType,
|
|
381
|
+
moderated_status: z.string().nullable(),
|
|
382
|
+
entity_id: z.string().nullable(),
|
|
383
|
+
fields: z.array(VirtualField)
|
|
384
|
+
}).loose();
|
|
385
|
+
const QueryMetadata = z.object({
|
|
386
|
+
databases: z.array(Database),
|
|
387
|
+
tables: z.array(z.union([TableQueryMetadata, VirtualTable])),
|
|
388
|
+
fields: z.array(Field),
|
|
389
|
+
snippets: z.array(Snippet)
|
|
390
|
+
}).loose();
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region ../client/src/resources/card.ts
|
|
393
|
+
const CardApiList = z.array(Card);
|
|
394
|
+
function cardResource(transport) {
|
|
395
|
+
/** List cards. `f` picks a server-side preset; `model_id` scopes the presets that need an id. */
|
|
396
|
+
async function list(params = {}, options = {}) {
|
|
397
|
+
await transport.require("card.list", options);
|
|
398
|
+
return {
|
|
399
|
+
data: await transport.requestParsed(CardApiList, "/api/card", {
|
|
400
|
+
...options,
|
|
401
|
+
query: {
|
|
402
|
+
f: params.f,
|
|
403
|
+
model_id: params.model_id
|
|
404
|
+
}
|
|
405
|
+
}),
|
|
406
|
+
total: null
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/** Get one card by id. */
|
|
410
|
+
async function get(id, options = {}) {
|
|
411
|
+
await transport.require("card.get", options);
|
|
412
|
+
return transport.requestParsed(Card, `/api/card/${id}`, { ...options });
|
|
413
|
+
}
|
|
414
|
+
/** Create a card — a question, a model, or a metric — from a full card body. */
|
|
415
|
+
async function create(params, options = {}) {
|
|
416
|
+
await transport.require("card.create", options);
|
|
417
|
+
return transport.requestParsed(Card, "/api/card", {
|
|
418
|
+
...options,
|
|
419
|
+
method: "POST",
|
|
420
|
+
body: params
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/** Update a card by id, patching only the fields the body carries. */
|
|
424
|
+
async function update(id, params, options = {}) {
|
|
425
|
+
await transport.require("card.update", options);
|
|
426
|
+
return transport.requestParsed(Card, `/api/card/${id}`, {
|
|
427
|
+
...options,
|
|
428
|
+
method: "PUT",
|
|
429
|
+
body: params
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/** Archive (soft-delete) a card by id. Metabase models this as an update, not its own endpoint. */
|
|
433
|
+
async function archive(id, options = {}) {
|
|
434
|
+
await transport.require("card.archive", options);
|
|
435
|
+
return update(id, { archived: true }, options);
|
|
436
|
+
}
|
|
437
|
+
/** Run a saved card and return the query result envelope. */
|
|
438
|
+
async function query(id, params, options = {}) {
|
|
439
|
+
await transport.require("card.query", options);
|
|
440
|
+
return transport.requestParsed(CardQueryResult, `/api/card/${id}/query`, {
|
|
441
|
+
...options,
|
|
442
|
+
method: "POST",
|
|
443
|
+
body: { parameters: params.parameters }
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Run a saved card and stream its result as a download. Unlike `query`, this endpoint takes a
|
|
448
|
+
* form-encoded body and answers bytes, so a caller consumes the stream rather than a value.
|
|
449
|
+
*/
|
|
450
|
+
async function exportQuery(id, format, params, options = {}) {
|
|
451
|
+
await transport.require("card.exportQuery", options);
|
|
452
|
+
const body = new URLSearchParams({
|
|
453
|
+
parameters: JSON.stringify(params.parameters),
|
|
454
|
+
format_rows: String(params.format_rows),
|
|
455
|
+
pivot_results: String(params.pivot_results)
|
|
456
|
+
});
|
|
457
|
+
return transport.requestStream(`/api/card/${id}/query/${format}`, {
|
|
458
|
+
...options,
|
|
459
|
+
method: "POST",
|
|
460
|
+
body
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Get all of the required query metadata for a saved card: the databases, tables, fields and
|
|
465
|
+
* snippets its query references, plus the card's own metadata for a model or a native query.
|
|
466
|
+
*/
|
|
467
|
+
async function queryMetadata(id, options = {}) {
|
|
468
|
+
await transport.require("card.queryMetadata", options);
|
|
469
|
+
return transport.requestParsed(QueryMetadata, `/api/card/${id}/query_metadata`, { ...options });
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
list,
|
|
473
|
+
get,
|
|
474
|
+
create,
|
|
475
|
+
update,
|
|
476
|
+
archive,
|
|
477
|
+
query,
|
|
478
|
+
exportQuery,
|
|
479
|
+
queryMetadata
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
async function* paginatePages(client, path, itemSchema, opts = {}) {
|
|
483
|
+
const envelopeSchema = paginatedEnvelopeSchema(itemSchema);
|
|
484
|
+
const pageSize = opts.pageSize ?? 50;
|
|
485
|
+
if (!Number.isInteger(pageSize) || pageSize < 1) throw new InternalError(`pageSize must be an integer of at least 1, got ${pageSize}`);
|
|
486
|
+
const start = opts.offset ?? 0;
|
|
487
|
+
const max = opts.max ?? Number.POSITIVE_INFINITY;
|
|
488
|
+
let taken = 0;
|
|
489
|
+
let previousHead = null;
|
|
490
|
+
while (taken < max) {
|
|
491
|
+
const remaining = max - taken;
|
|
492
|
+
const limit = Math.min(pageSize, remaining);
|
|
493
|
+
const offset = start + taken;
|
|
494
|
+
const envelope = await client.requestParsed(envelopeSchema, path, {
|
|
495
|
+
query: {
|
|
496
|
+
...opts.query,
|
|
497
|
+
limit,
|
|
498
|
+
offset
|
|
499
|
+
},
|
|
500
|
+
...opts.signal !== void 0 && { signal: opts.signal }
|
|
501
|
+
});
|
|
502
|
+
const items = envelope.data.length > remaining ? envelope.data.slice(0, remaining) : envelope.data;
|
|
503
|
+
const total = envelope.total ?? null;
|
|
504
|
+
const head = items.length === 0 ? null : JSON.stringify(items[0]);
|
|
505
|
+
if (head !== null && head === previousHead) throw new ConfigError(`the endpoint returned the same first row at offset ${offset} as at the previous offset; it is ignoring the offset parameter, so paging cannot advance`);
|
|
506
|
+
previousHead = head;
|
|
507
|
+
yield {
|
|
508
|
+
items,
|
|
509
|
+
total
|
|
510
|
+
};
|
|
511
|
+
if (items.length === 0) return;
|
|
512
|
+
const consumed = taken + items.length;
|
|
513
|
+
if (total !== null && start + consumed === total) return;
|
|
514
|
+
taken = consumed;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function paginatedEnvelopeSchema(itemSchema) {
|
|
518
|
+
return z.object({
|
|
519
|
+
data: z.array(itemSchema),
|
|
520
|
+
total: z.number().int().nonnegative().nullable().optional(),
|
|
521
|
+
limit: z.number().int().nonnegative().optional(),
|
|
522
|
+
offset: z.number().int().nonnegative().optional()
|
|
523
|
+
}).loose();
|
|
524
|
+
}
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region ../client/src/resources/collection.ts
|
|
527
|
+
const CollectionApiList = z.array(Collection);
|
|
528
|
+
const CollectionTreeApiList = z.array(CollectionTreeNode);
|
|
529
|
+
const COLLECTION_LIST_QUERY = {
|
|
530
|
+
all: {},
|
|
531
|
+
archived: { archived: true },
|
|
532
|
+
personal: { "personal-only": true }
|
|
533
|
+
};
|
|
534
|
+
const DEFAULT_LIST_FILTER = "all";
|
|
535
|
+
function refPath(ref) {
|
|
536
|
+
return encodeURIComponent(ref);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* List collections including the Library and its children, parsing each through the caller's own
|
|
540
|
+
* projection. `Collection` pins `type`, `namespace` and `authority_level` to closed enums, so a
|
|
541
|
+
* consumer reading a few fields off every collection on the instance narrows here: one collection
|
|
542
|
+
* carrying a server value outside those sets then costs nothing to a caller that never reads the
|
|
543
|
+
* field.
|
|
544
|
+
*/
|
|
545
|
+
async function listCollectionsWithLibrary(transport, schema, options = {}) {
|
|
546
|
+
return transport.requestParsed(z.array(schema), "/api/collection", {
|
|
547
|
+
...options,
|
|
548
|
+
query: { "include-library": true }
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function collectionResource(transport) {
|
|
552
|
+
/** List collections. `filter` picks a server-side preset: everything, archived, or personal. */
|
|
553
|
+
async function list(params = {}, options = {}) {
|
|
554
|
+
await transport.require("collection.list", options);
|
|
555
|
+
return {
|
|
556
|
+
data: await transport.requestParsed(CollectionApiList, "/api/collection", {
|
|
557
|
+
...options,
|
|
558
|
+
query: COLLECTION_LIST_QUERY[params.filter ?? DEFAULT_LIST_FILTER]
|
|
559
|
+
}),
|
|
560
|
+
total: null
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* List collections including the Library and its children, which the plain listing omits.
|
|
565
|
+
* The Library's own children carry neither `type` nor `is_remote_synced`, so this is where a
|
|
566
|
+
* caller resolves both.
|
|
567
|
+
*/
|
|
568
|
+
async function listWithLibrary(options = {}) {
|
|
569
|
+
await transport.require("collection.listWithLibrary", options);
|
|
570
|
+
return {
|
|
571
|
+
data: await listCollectionsWithLibrary(transport, Collection, options),
|
|
572
|
+
total: null
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/** Get one collection by id, entity id, or the `root`/`trash` alias. */
|
|
576
|
+
async function get(ref, options = {}) {
|
|
577
|
+
await transport.require("collection.get", options);
|
|
578
|
+
return transport.requestParsed(Collection, `/api/collection/${refPath(ref)}`, { ...options });
|
|
579
|
+
}
|
|
580
|
+
/** Create a new collection. */
|
|
581
|
+
async function create(params, options = {}) {
|
|
582
|
+
await transport.require("collection.create", options);
|
|
583
|
+
return transport.requestParsed(Collection, "/api/collection", {
|
|
584
|
+
...options,
|
|
585
|
+
method: "POST",
|
|
586
|
+
body: params
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
/** Update a collection, patching only the fields the body carries. */
|
|
590
|
+
async function update(ref, params, options = {}) {
|
|
591
|
+
await transport.require("collection.update", options);
|
|
592
|
+
return transport.requestParsed(Collection, `/api/collection/${refPath(ref)}`, {
|
|
593
|
+
...options,
|
|
594
|
+
method: "PUT",
|
|
595
|
+
body: params
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
/** Archive (soft-delete) a collection. Metabase models this as an update, not its own endpoint. */
|
|
599
|
+
async function archive(ref, options = {}) {
|
|
600
|
+
await transport.require("collection.archive", options);
|
|
601
|
+
return update(ref, { archived: true }, options);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Walk the items inside a collection one page at a time. This endpoint pages on the server, so
|
|
605
|
+
* the caller consumes pages rather than a single list and decides how far to pull.
|
|
606
|
+
*/
|
|
607
|
+
async function* itemPages(ref, params = {}, options = {}) {
|
|
608
|
+
await transport.require("collection.itemPages", options);
|
|
609
|
+
yield* paginatePages(transport, `/api/collection/${refPath(ref)}/items`, CollectionItem, {
|
|
610
|
+
query: {
|
|
611
|
+
models: params.models,
|
|
612
|
+
archived: params.archived,
|
|
613
|
+
pinned_state: params.pinned_state
|
|
614
|
+
},
|
|
615
|
+
...options.offset !== void 0 && { offset: options.offset },
|
|
616
|
+
...options.max !== void 0 && { max: options.max },
|
|
617
|
+
...options.pageSize !== void 0 && { pageSize: options.pageSize },
|
|
618
|
+
...options.signal !== void 0 && { signal: options.signal }
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
/** Fetch the collection hierarchy as a forest of nested nodes. */
|
|
622
|
+
async function tree(options = {}) {
|
|
623
|
+
await transport.require("collection.tree", options);
|
|
624
|
+
return {
|
|
625
|
+
data: await transport.requestParsed(CollectionTreeApiList, "/api/collection/tree", { ...options }),
|
|
626
|
+
total: null
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
list,
|
|
631
|
+
listWithLibrary,
|
|
632
|
+
get,
|
|
633
|
+
create,
|
|
634
|
+
update,
|
|
635
|
+
archive,
|
|
636
|
+
itemPages,
|
|
637
|
+
tree
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region ../client/src/resources/csv-upload.ts
|
|
642
|
+
const CSV_CONTENT_TYPE = "text/csv";
|
|
643
|
+
function buildCsvFormData(file) {
|
|
644
|
+
const form = new FormData();
|
|
645
|
+
form.append("file", new Blob([file.bytes], { type: CSV_CONTENT_TYPE }), file.filename);
|
|
646
|
+
return form;
|
|
647
|
+
}
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region ../client/src/resources/content-translation.ts
|
|
650
|
+
const CONTENT_TRANSLATION_PATH = "/api/ee/content-translation";
|
|
651
|
+
function contentTranslationResource(transport) {
|
|
652
|
+
/** Download the complete content translation dictionary as CSV. */
|
|
653
|
+
async function download(options = {}) {
|
|
654
|
+
await transport.require("contentTranslation.download", options);
|
|
655
|
+
return transport.requestStream(`${CONTENT_TRANSLATION_PATH}/csv`, options);
|
|
656
|
+
}
|
|
657
|
+
/** Replace the complete content translation dictionary with the attached CSV. */
|
|
658
|
+
async function upload(file, options = {}) {
|
|
659
|
+
await transport.require("contentTranslation.upload", options);
|
|
660
|
+
const form = buildCsvFormData(file);
|
|
661
|
+
return transport.requestParsed(ContentTranslationUploadResult, `${CONTENT_TRANSLATION_PATH}/upload-dictionary`, {
|
|
662
|
+
...options,
|
|
663
|
+
method: "POST",
|
|
664
|
+
body: form
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
download,
|
|
669
|
+
upload
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region ../client/src/resources/dashboard.ts
|
|
674
|
+
const DashboardApiList = z.array(Dashboard);
|
|
675
|
+
const PreflightDashcard = z.looseObject({ card_id: z.number().int().nullable().optional() });
|
|
676
|
+
function dashboardResource(transport) {
|
|
677
|
+
const card = cardResource(transport);
|
|
678
|
+
/** List dashboards. `f` picks a server-side preset. */
|
|
679
|
+
async function list(params = {}, options = {}) {
|
|
680
|
+
await transport.require("dashboard.list", options);
|
|
681
|
+
return {
|
|
682
|
+
data: await transport.requestParsed(DashboardApiList, "/api/dashboard", {
|
|
683
|
+
...options,
|
|
684
|
+
query: { f: params.f }
|
|
685
|
+
}),
|
|
686
|
+
total: null
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
/** Get one dashboard by id, with its dashcards, tabs and parameters. */
|
|
690
|
+
async function get(id, options = {}) {
|
|
691
|
+
await transport.require("dashboard.get", options);
|
|
692
|
+
return transport.requestParsed(DashboardDetail, `/api/dashboard/${id}`, { ...options });
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Create a dashboard. The endpoint accepts no dashcards or tabs — a follow-up `update` is what
|
|
696
|
+
* puts content on a fresh dashboard.
|
|
697
|
+
*/
|
|
698
|
+
async function create(params, options = {}) {
|
|
699
|
+
await transport.require("dashboard.create", options);
|
|
700
|
+
return transport.requestParsed(Dashboard, "/api/dashboard", {
|
|
701
|
+
...options,
|
|
702
|
+
method: "POST",
|
|
703
|
+
body: params
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Update a dashboard by id, patching only the fields the body carries. `dashcards` and `tabs` are
|
|
708
|
+
* full replacements: whatever the body omits is deleted from the dashboard.
|
|
709
|
+
*/
|
|
710
|
+
async function update(id, params, options = {}) {
|
|
711
|
+
await transport.require("dashboard.update", options);
|
|
712
|
+
return transport.requestParsed(DashboardDetail, `/api/dashboard/${id}`, {
|
|
713
|
+
...options,
|
|
714
|
+
method: "PUT",
|
|
715
|
+
body: params
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Copy a dashboard, with its tabs and dashcards, into `collection_id` (root when absent). A deep
|
|
720
|
+
* copy duplicates the cards it can; otherwise the copy references them, except dashboard
|
|
721
|
+
* questions, which are always duplicated. Cards the caller cannot read are left behind and
|
|
722
|
+
* reported as `uncopied`.
|
|
723
|
+
*/
|
|
724
|
+
async function copy(id, params = {}, options = {}) {
|
|
725
|
+
await transport.require("dashboard.copy", options);
|
|
726
|
+
return transport.requestParsed(DashboardCopy, `/api/dashboard/${id}/copy`, {
|
|
727
|
+
...options,
|
|
728
|
+
method: "POST",
|
|
729
|
+
body: params
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Archive (soft-delete) a dashboard by id. Metabase models this as an update, not its own
|
|
734
|
+
* endpoint. The answer is read as `Dashboard` rather than the `DashboardDetail` `update` returns:
|
|
735
|
+
* an archive is a state change on the dashboard itself, and its dashcards are not part of it.
|
|
736
|
+
*/
|
|
737
|
+
async function archive(id, options = {}) {
|
|
738
|
+
await transport.require("dashboard.archive", options);
|
|
739
|
+
return transport.requestParsed(Dashboard, `/api/dashboard/${id}`, {
|
|
740
|
+
...options,
|
|
741
|
+
method: "PUT",
|
|
742
|
+
body: { archived: true }
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Patch one dashcard on a dashboard. Metabase exposes no dashcard endpoint and its dashboard PUT
|
|
747
|
+
* is a full replace of `dashcards`, so the patch is read-modify-written over the dashboard as it
|
|
748
|
+
* currently stands.
|
|
749
|
+
*/
|
|
750
|
+
async function updateDashcard(dashboardId, dashcardId, params, options = {}) {
|
|
751
|
+
await transport.require("dashboard.updateDashcard", options);
|
|
752
|
+
const dashboard = await get(dashboardId, options);
|
|
753
|
+
const target = dashboard.dashcards.find((dashcard) => dashcard.id === dashcardId);
|
|
754
|
+
if (target === void 0) throw new ConfigError(`dashcard ${dashcardId} not found on dashboard ${dashboardId}`);
|
|
755
|
+
const patched = Dashcard.parse({
|
|
756
|
+
...target,
|
|
757
|
+
...params
|
|
758
|
+
});
|
|
759
|
+
const refreshed = (await update(dashboardId, { dashcards: dashboard.dashcards.map((dashcard) => stripEntityId(dashcard.id === dashcardId ? patched : dashcard)) }, options)).dashcards.find((dashcard) => dashcard.id === dashcardId);
|
|
760
|
+
if (refreshed === void 0) throw new Error(`PUT /api/dashboard/${dashboardId}: dashcard ${dashcardId} missing from response`);
|
|
761
|
+
return refreshed;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Check every card a `dashcards` body references before writing it, through `GET /api/card/{id}`
|
|
765
|
+
* once per distinct id. Reports one issue per reference whose card is missing, archived, or
|
|
766
|
+
* unreadable, so an empty result means the body is safe to send; an id the server answers for in
|
|
767
|
+
* any other way propagates as the failure it was.
|
|
768
|
+
*/
|
|
769
|
+
async function checkCardReferences(dashcards, options = {}) {
|
|
770
|
+
await transport.require("dashboard.checkCardReferences", options);
|
|
771
|
+
const grouped = groupByCardId(collectCardReferences(dashcards));
|
|
772
|
+
return (await Promise.all(Array.from(grouped.entries()).map(async ([cardId, references]) => ({
|
|
773
|
+
references,
|
|
774
|
+
problem: await inspectCard(cardId, options)
|
|
775
|
+
})))).flatMap((check) => {
|
|
776
|
+
const problem = check.problem;
|
|
777
|
+
if (problem === null) return [];
|
|
778
|
+
return check.references.map((reference) => ({
|
|
779
|
+
...reference,
|
|
780
|
+
problem
|
|
781
|
+
}));
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
async function inspectCard(cardId, options) {
|
|
785
|
+
try {
|
|
786
|
+
return (await card.get(cardId, options)).archived ? { reason: "archived" } : null;
|
|
787
|
+
} catch (error) {
|
|
788
|
+
if (!(error instanceof HttpError)) throw error;
|
|
789
|
+
if (error.kind === "resource-missing") return { reason: "missing" };
|
|
790
|
+
if (error.kind === "auth") return {
|
|
791
|
+
reason: "unreadable",
|
|
792
|
+
detail: error.userMessage
|
|
793
|
+
};
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Fetch the selectable values for a dashboard parameter, through the chain-filter value endpoint.
|
|
799
|
+
*/
|
|
800
|
+
async function parameterValues(dashboardId, parameterId, options = {}) {
|
|
801
|
+
await transport.require("dashboard.parameterValues", options);
|
|
802
|
+
return transport.requestParsed(ParameterValues, `/api/dashboard/${dashboardId}/params/${encodeURIComponent(parameterId)}/values`, { ...options });
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Search a dashboard parameter's selectable values: the server returns only values containing the
|
|
806
|
+
* substring, capped at the first 1000 matches.
|
|
807
|
+
*/
|
|
808
|
+
async function searchParameterValues(dashboardId, parameterId, query, options = {}) {
|
|
809
|
+
await transport.require("dashboard.searchParameterValues", options);
|
|
810
|
+
const parameter = encodeURIComponent(parameterId);
|
|
811
|
+
return transport.requestParsed(ParameterValues, `/api/dashboard/${dashboardId}/params/${parameter}/search/${encodeURIComponent(query)}`, { ...options });
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
list,
|
|
815
|
+
get,
|
|
816
|
+
create,
|
|
817
|
+
update,
|
|
818
|
+
archive,
|
|
819
|
+
copy,
|
|
820
|
+
updateDashcard,
|
|
821
|
+
checkCardReferences,
|
|
822
|
+
parameterValues,
|
|
823
|
+
searchParameterValues
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function collectCardReferences(dashcards) {
|
|
827
|
+
if (dashcards === void 0 || dashcards === null) return [];
|
|
828
|
+
const references = [];
|
|
829
|
+
dashcards.forEach((dashcard, index) => {
|
|
830
|
+
const parsed = PreflightDashcard.safeParse(dashcard);
|
|
831
|
+
if (!parsed.success) return;
|
|
832
|
+
const cardId = parsed.data.card_id;
|
|
833
|
+
if (typeof cardId === "number" && cardId > 0) references.push({
|
|
834
|
+
cardId,
|
|
835
|
+
path: `/dashcards/${index}/card_id`
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
return references;
|
|
839
|
+
}
|
|
840
|
+
function groupByCardId(references) {
|
|
841
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
842
|
+
for (const reference of references) {
|
|
843
|
+
const existing = grouped.get(reference.cardId);
|
|
844
|
+
if (existing === void 0) grouped.set(reference.cardId, [reference]);
|
|
845
|
+
else existing.push(reference);
|
|
846
|
+
}
|
|
847
|
+
return grouped;
|
|
848
|
+
}
|
|
849
|
+
function stripEntityId(dashcard) {
|
|
850
|
+
const { entity_id: _entity_id, id, size_x, size_y, row, col, ...rest } = dashcard;
|
|
851
|
+
return {
|
|
852
|
+
...rest,
|
|
853
|
+
id,
|
|
854
|
+
size_x,
|
|
855
|
+
size_y,
|
|
856
|
+
row,
|
|
857
|
+
col
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region ../client/src/resources/database.ts
|
|
862
|
+
const DatabaseApiList = z.object({
|
|
863
|
+
data: z.array(Database),
|
|
864
|
+
total: z.number().int().nonnegative()
|
|
865
|
+
}).loose();
|
|
866
|
+
const DatabaseTaskAck = z.object({ status: z.literal("ok") });
|
|
867
|
+
const DatabaseApiSchemaList = z.array(z.string());
|
|
868
|
+
const DatabaseApiSchemaTableList = z.array(Table);
|
|
869
|
+
const SYNC_COMPLETE = "complete";
|
|
870
|
+
function databaseResource(transport) {
|
|
871
|
+
/** List databases. `include` hydrates each database's tables; `saved` adds the Saved Questions virtual database. */
|
|
872
|
+
async function list(params = {}, options = {}) {
|
|
873
|
+
await transport.require("database.list", options);
|
|
874
|
+
const response = await transport.requestParsed(DatabaseApiList, "/api/database", {
|
|
875
|
+
...options,
|
|
876
|
+
query: {
|
|
877
|
+
include: params.include,
|
|
878
|
+
saved: params.saved
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
return {
|
|
882
|
+
data: response.data,
|
|
883
|
+
total: response.total
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
/** Get one database by id. `include` hydrates its tables, and `tables.fields` their fields too. */
|
|
887
|
+
async function get(id, params = {}, options = {}) {
|
|
888
|
+
await transport.require("database.get", options);
|
|
889
|
+
return transport.requestParsed(Database, `/api/database/${id}`, {
|
|
890
|
+
...options,
|
|
891
|
+
query: { include: params.include }
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
/** List the schema names in a database. */
|
|
895
|
+
async function schemas(id, options = {}) {
|
|
896
|
+
await transport.require("database.schemas", options);
|
|
897
|
+
return {
|
|
898
|
+
data: await transport.requestParsed(DatabaseApiSchemaList, `/api/database/${id}/schemas`, { ...options }),
|
|
899
|
+
total: null
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
/** List the tables one schema of a database holds. */
|
|
903
|
+
async function schemaTables(id, schema, options = {}) {
|
|
904
|
+
await transport.require("database.schemaTables", options);
|
|
905
|
+
return {
|
|
906
|
+
data: await transport.requestParsed(DatabaseApiSchemaTableList, `/api/database/${id}/schema/${encodeURIComponent(schema)}`, { ...options }),
|
|
907
|
+
total: null
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Trigger a manual schema sync for a database. The endpoint queues the sync and returns at once;
|
|
912
|
+
* pass `wait` to poll the database until it reports its initial sync complete.
|
|
913
|
+
*/
|
|
914
|
+
async function syncSchema(id, params = {}, options = {}) {
|
|
915
|
+
await transport.require("database.syncSchema", options);
|
|
916
|
+
const ack = await transport.requestParsed(DatabaseTaskAck, `/api/database/${id}/sync_schema`, {
|
|
917
|
+
...options,
|
|
918
|
+
method: "POST"
|
|
919
|
+
});
|
|
920
|
+
const wait = params.wait;
|
|
921
|
+
if (wait === void 0) return {
|
|
922
|
+
id,
|
|
923
|
+
status: ack.status
|
|
924
|
+
};
|
|
925
|
+
const database = await pollUntil(async (signal) => transport.requestParsed(Database, `/api/database/${id}`, {
|
|
926
|
+
...options,
|
|
927
|
+
signal
|
|
928
|
+
}), (candidate) => candidate.initial_sync_status === SYNC_COMPLETE, wait);
|
|
929
|
+
return {
|
|
930
|
+
id,
|
|
931
|
+
status: ack.status,
|
|
932
|
+
initial_sync_status: database.initial_sync_status ?? null
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
/** Trigger a rescan of the cached field values across a database. */
|
|
936
|
+
async function rescanValues(id, options = {}) {
|
|
937
|
+
await transport.require("database.rescanValues", options);
|
|
938
|
+
return {
|
|
939
|
+
id,
|
|
940
|
+
status: (await transport.requestParsed(DatabaseTaskAck, `/api/database/${id}/rescan_values`, {
|
|
941
|
+
...options,
|
|
942
|
+
method: "POST"
|
|
943
|
+
})).status
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
list,
|
|
948
|
+
get,
|
|
949
|
+
schemas,
|
|
950
|
+
schemaTables,
|
|
951
|
+
syncSchema,
|
|
952
|
+
rescanValues
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
//#endregion
|
|
956
|
+
//#region ../client/src/resources/dataset.ts
|
|
957
|
+
function datasetResource(transport) {
|
|
958
|
+
/**
|
|
959
|
+
* Run an ad-hoc query and return its result envelope. The body is a whole query — MBQL 5, legacy
|
|
960
|
+
* MBQL, or native — rather than a reference to a saved one, so nothing here is a card.
|
|
961
|
+
*/
|
|
962
|
+
async function query(body, options = {}) {
|
|
963
|
+
await transport.require("dataset.query", options);
|
|
964
|
+
return transport.requestParsed(CardQueryResult, "/api/dataset", {
|
|
965
|
+
...options,
|
|
966
|
+
method: "POST",
|
|
967
|
+
body
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Fetch a native version of an MBQL query. `pretty` rides in the same body as the query and
|
|
972
|
+
* defaults server-side to true.
|
|
973
|
+
*/
|
|
974
|
+
async function native(datasetQuery, params = {}, options = {}) {
|
|
975
|
+
await transport.require("dataset.native", options);
|
|
976
|
+
return transport.requestParsed(CompiledQuery, "/api/dataset/native", {
|
|
977
|
+
...options,
|
|
978
|
+
method: "POST",
|
|
979
|
+
body: {
|
|
980
|
+
...datasetQuery,
|
|
981
|
+
pretty: params.pretty
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Get all of the required query metadata for an ad-hoc query: the databases, tables, fields and
|
|
987
|
+
* snippets it references. `{ settings: { "include-sensitive-fields": true } }` inside the query
|
|
988
|
+
* widens the field filter.
|
|
989
|
+
*/
|
|
990
|
+
async function queryMetadata(datasetQuery, options = {}) {
|
|
991
|
+
await transport.require("dataset.queryMetadata", options);
|
|
992
|
+
return transport.requestParsed(QueryMetadata, "/api/dataset/query_metadata", {
|
|
993
|
+
...options,
|
|
994
|
+
method: "POST",
|
|
995
|
+
body: datasetQuery
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Run an ad-hoc query and stream its result as a download. Unlike `query`, this endpoint answers
|
|
1000
|
+
* bytes, so a caller consumes the stream rather than a value.
|
|
1001
|
+
*/
|
|
1002
|
+
async function exportQuery(format, params, options = {}) {
|
|
1003
|
+
await transport.require("dataset.exportQuery", options);
|
|
1004
|
+
return transport.requestStream(`/api/dataset/${format}`, {
|
|
1005
|
+
...options,
|
|
1006
|
+
method: "POST",
|
|
1007
|
+
body: {
|
|
1008
|
+
query: params.query,
|
|
1009
|
+
format_rows: params.format_rows,
|
|
1010
|
+
pivot_results: params.pivot_results
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
return {
|
|
1015
|
+
query,
|
|
1016
|
+
native,
|
|
1017
|
+
queryMetadata,
|
|
1018
|
+
exportQuery
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region ../client/src/domain/dependency.ts
|
|
1023
|
+
const DependencyType = z.enum([
|
|
1024
|
+
"table",
|
|
1025
|
+
"card",
|
|
1026
|
+
"snippet",
|
|
1027
|
+
"transform",
|
|
1028
|
+
"dashboard",
|
|
1029
|
+
"document",
|
|
1030
|
+
"sandbox",
|
|
1031
|
+
"segment",
|
|
1032
|
+
"measure"
|
|
1033
|
+
]);
|
|
1034
|
+
const DependencyUsageType = z.enum([
|
|
1035
|
+
"table",
|
|
1036
|
+
"snippet",
|
|
1037
|
+
"transform",
|
|
1038
|
+
"dashboard",
|
|
1039
|
+
"document",
|
|
1040
|
+
"sandbox",
|
|
1041
|
+
"segment",
|
|
1042
|
+
"question",
|
|
1043
|
+
"model",
|
|
1044
|
+
"metric",
|
|
1045
|
+
"measure"
|
|
1046
|
+
]);
|
|
1047
|
+
const DependencyFindingErrorType = z.enum([
|
|
1048
|
+
"missing-column",
|
|
1049
|
+
"missing-table-alias",
|
|
1050
|
+
"missing-table",
|
|
1051
|
+
"missing-card",
|
|
1052
|
+
"duplicate-column",
|
|
1053
|
+
"syntax-error",
|
|
1054
|
+
"validation-exception-error"
|
|
1055
|
+
]);
|
|
1056
|
+
const DependencyErrorSourceType = z.enum([
|
|
1057
|
+
"table",
|
|
1058
|
+
"card",
|
|
1059
|
+
"transform"
|
|
1060
|
+
]);
|
|
1061
|
+
z.enum([
|
|
1062
|
+
"name",
|
|
1063
|
+
"location",
|
|
1064
|
+
"view-count"
|
|
1065
|
+
]);
|
|
1066
|
+
z.enum([
|
|
1067
|
+
"name",
|
|
1068
|
+
"location",
|
|
1069
|
+
"dependents-with-errors",
|
|
1070
|
+
"dependents-errors"
|
|
1071
|
+
]);
|
|
1072
|
+
const DependencyCollectionRef = z.object({
|
|
1073
|
+
id: CollectionId,
|
|
1074
|
+
name: z.string(),
|
|
1075
|
+
authority_level: CollectionAuthorityLevel.nullable(),
|
|
1076
|
+
is_personal: z.boolean().optional()
|
|
1077
|
+
}).loose();
|
|
1078
|
+
const DependencyContainerRef = z.object({
|
|
1079
|
+
id: z.number().int(),
|
|
1080
|
+
name: z.string()
|
|
1081
|
+
}).loose();
|
|
1082
|
+
const DependencyEntityData = z.object({
|
|
1083
|
+
name: z.string().nullable().optional(),
|
|
1084
|
+
display_name: z.string().nullable().optional(),
|
|
1085
|
+
description: z.string().nullable().optional(),
|
|
1086
|
+
type: CardType.nullable().optional(),
|
|
1087
|
+
display: z.string().nullable().optional(),
|
|
1088
|
+
database_id: z.number().int().nullable().optional(),
|
|
1089
|
+
db_id: z.number().int().nullable().optional(),
|
|
1090
|
+
schema: z.string().nullable().optional(),
|
|
1091
|
+
collection_id: CollectionId.nullable().optional(),
|
|
1092
|
+
collection: DependencyCollectionRef.nullable().optional(),
|
|
1093
|
+
dashboard_id: z.number().int().nullable().optional(),
|
|
1094
|
+
dashboard: DependencyContainerRef.nullable().optional(),
|
|
1095
|
+
document_id: z.number().int().nullable().optional(),
|
|
1096
|
+
document: DependencyContainerRef.nullable().optional(),
|
|
1097
|
+
table_id: z.number().int().nullable().optional(),
|
|
1098
|
+
view_count: z.number().int().nullable().optional()
|
|
1099
|
+
}).loose();
|
|
1100
|
+
const DependencyEntity = z.object({
|
|
1101
|
+
id: z.number().int(),
|
|
1102
|
+
type: DependencyType,
|
|
1103
|
+
data: DependencyEntityData
|
|
1104
|
+
}).loose();
|
|
1105
|
+
const DependencyNode = DependencyEntity.extend({ dependents_count: z.partialRecord(DependencyUsageType, z.number().int()).nullable() });
|
|
1106
|
+
const DependencyFindingError = z.object({
|
|
1107
|
+
id: z.number().int(),
|
|
1108
|
+
analyzed_entity_type: DependencyType,
|
|
1109
|
+
analyzed_entity_id: z.number().int(),
|
|
1110
|
+
error_type: DependencyFindingErrorType,
|
|
1111
|
+
error_detail: z.string().nullable().optional(),
|
|
1112
|
+
source_entity_type: DependencyErrorSourceType.nullable().optional(),
|
|
1113
|
+
source_entity_id: z.number().int().nullable().optional()
|
|
1114
|
+
}).loose();
|
|
1115
|
+
const BreakingSource = DependencyNode.extend({ dependents_errors: z.array(DependencyFindingError).nullable().optional() });
|
|
1116
|
+
const DependencyEdge = z.object({
|
|
1117
|
+
from_entity_type: DependencyType,
|
|
1118
|
+
from_entity_id: z.number().int(),
|
|
1119
|
+
to_entity_type: DependencyType,
|
|
1120
|
+
to_entity_id: z.number().int()
|
|
1121
|
+
}).loose();
|
|
1122
|
+
const DependencyGraph = z.object({
|
|
1123
|
+
nodes: z.array(DependencyNode),
|
|
1124
|
+
edges: z.array(DependencyEdge)
|
|
1125
|
+
}).loose();
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region ../client/src/resources/dependency.ts
|
|
1128
|
+
const DependencyNodeApiList = z.array(DependencyNode);
|
|
1129
|
+
const DependencyEntityApiList = z.array(DependencyEntity);
|
|
1130
|
+
const RENAMED_FILTERS = [
|
|
1131
|
+
"dependent-types",
|
|
1132
|
+
"dependent-card-types",
|
|
1133
|
+
"card-types",
|
|
1134
|
+
"include-personal-collections",
|
|
1135
|
+
"sort-column",
|
|
1136
|
+
"sort-direction"
|
|
1137
|
+
];
|
|
1138
|
+
function filterFeatures(params) {
|
|
1139
|
+
return RENAMED_FILTERS.some((key) => params[key] !== void 0) ? ["dependencyKebabCaseFilters"] : [];
|
|
1140
|
+
}
|
|
1141
|
+
function dependencyResource(transport) {
|
|
1142
|
+
/**
|
|
1143
|
+
* The upstream dependency graph of one entity. `nodes` holds the starting entity plus every
|
|
1144
|
+
* entity it depends on, directly or transitively; each edge runs from the dependent to what it
|
|
1145
|
+
* depends on.
|
|
1146
|
+
*/
|
|
1147
|
+
async function graph(type, id, options = {}) {
|
|
1148
|
+
await transport.require("dependency.graph", options);
|
|
1149
|
+
return transport.requestParsed(DependencyGraph, "/api/ee/dependencies/graph", {
|
|
1150
|
+
...options,
|
|
1151
|
+
query: {
|
|
1152
|
+
type,
|
|
1153
|
+
id
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* The entities that depend directly on one entity. `dependent-types` and `dependent-card-types`
|
|
1159
|
+
* narrow which kinds are listed, `broken` keeps only those whose query the entity has broken,
|
|
1160
|
+
* `query` matches against names and locations, `include-personal-collections` admits content in
|
|
1161
|
+
* personal collections, and `sort-column` / `sort-direction` order the result.
|
|
1162
|
+
*/
|
|
1163
|
+
async function dependents(type, id, params = {}, options = {}) {
|
|
1164
|
+
await transport.require("dependency.dependents", options);
|
|
1165
|
+
await transport.requireFeatures(filterFeatures(params), options);
|
|
1166
|
+
return {
|
|
1167
|
+
data: await transport.requestParsed(DependencyNodeApiList, "/api/ee/dependencies/graph/dependents", {
|
|
1168
|
+
...options,
|
|
1169
|
+
query: {
|
|
1170
|
+
type,
|
|
1171
|
+
id,
|
|
1172
|
+
"dependent-types": params["dependent-types"],
|
|
1173
|
+
"dependent-card-types": params["dependent-card-types"],
|
|
1174
|
+
broken: params.broken,
|
|
1175
|
+
query: params.query,
|
|
1176
|
+
"include-personal-collections": params["include-personal-collections"],
|
|
1177
|
+
"sort-column": params["sort-column"],
|
|
1178
|
+
"sort-direction": params["sort-direction"]
|
|
1179
|
+
}
|
|
1180
|
+
}),
|
|
1181
|
+
total: null
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* The dependents of one entity whose queries it has broken, as bare entities without a
|
|
1186
|
+
* dependents count. `dependent-types` and `dependent-card-types` narrow which kinds are listed,
|
|
1187
|
+
* `include-personal-collections` admits content in personal collections, and `sort-column` /
|
|
1188
|
+
* `sort-direction` order the result.
|
|
1189
|
+
*/
|
|
1190
|
+
async function broken(type, id, params = {}, options = {}) {
|
|
1191
|
+
await transport.require("dependency.broken", options);
|
|
1192
|
+
await transport.requireFeatures(filterFeatures(params), options);
|
|
1193
|
+
return {
|
|
1194
|
+
data: await transport.requestParsed(DependencyEntityApiList, "/api/ee/dependencies/graph/broken", {
|
|
1195
|
+
...options,
|
|
1196
|
+
query: {
|
|
1197
|
+
type,
|
|
1198
|
+
id,
|
|
1199
|
+
"dependent-types": params["dependent-types"],
|
|
1200
|
+
"dependent-card-types": params["dependent-card-types"],
|
|
1201
|
+
"include-personal-collections": params["include-personal-collections"],
|
|
1202
|
+
"sort-column": params["sort-column"],
|
|
1203
|
+
"sort-direction": params["sort-direction"]
|
|
1204
|
+
}
|
|
1205
|
+
}),
|
|
1206
|
+
total: null
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Walk the entities nothing depends on, one page at a time. `types` and `card-types` narrow
|
|
1211
|
+
* which kinds are listed, `query` matches against names and locations,
|
|
1212
|
+
* `include-personal-collections` admits content in personal collections, and `sort-column` /
|
|
1213
|
+
* `sort-direction` order the result. This endpoint pages on the server, so the caller consumes
|
|
1214
|
+
* pages and decides how far to pull.
|
|
1215
|
+
*/
|
|
1216
|
+
async function* unreferencedPages(params = {}, options = {}) {
|
|
1217
|
+
await transport.require("dependency.unreferencedPages", options);
|
|
1218
|
+
await transport.requireFeatures(filterFeatures(params), options);
|
|
1219
|
+
yield* paginatePages(transport, "/api/ee/dependencies/graph/unreferenced", DependencyNode, {
|
|
1220
|
+
query: itemListQuery(params),
|
|
1221
|
+
...options.offset !== void 0 && { offset: options.offset },
|
|
1222
|
+
...options.max !== void 0 && { max: options.max },
|
|
1223
|
+
...options.pageSize !== void 0 && { pageSize: options.pageSize },
|
|
1224
|
+
...options.signal !== void 0 && { signal: options.signal }
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Walk the entities whose dependents carry analysis errors, one page at a time, each with the
|
|
1229
|
+
* `dependents_errors` rows found downstream of it. The server lists cards and tables unless
|
|
1230
|
+
* `types` says otherwise; `card-types`, `query`, `include-personal-collections`, `sort-column`
|
|
1231
|
+
* and `sort-direction` read as on `unreferencedPages`. This endpoint pages on the server, so the
|
|
1232
|
+
* caller consumes pages and decides how far to pull.
|
|
1233
|
+
*/
|
|
1234
|
+
async function* breakingPages(params = {}, options = {}) {
|
|
1235
|
+
await transport.require("dependency.breakingPages", options);
|
|
1236
|
+
await transport.requireFeatures(filterFeatures(params), options);
|
|
1237
|
+
yield* paginatePages(transport, "/api/ee/dependencies/graph/breaking", BreakingSource, {
|
|
1238
|
+
query: itemListQuery(params),
|
|
1239
|
+
...options.offset !== void 0 && { offset: options.offset },
|
|
1240
|
+
...options.max !== void 0 && { max: options.max },
|
|
1241
|
+
...options.pageSize !== void 0 && { pageSize: options.pageSize },
|
|
1242
|
+
...options.signal !== void 0 && { signal: options.signal }
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
return {
|
|
1246
|
+
graph,
|
|
1247
|
+
dependents,
|
|
1248
|
+
broken,
|
|
1249
|
+
unreferencedPages,
|
|
1250
|
+
breakingPages
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
function itemListQuery(params) {
|
|
1254
|
+
return {
|
|
1255
|
+
types: params.types,
|
|
1256
|
+
"card-types": params["card-types"],
|
|
1257
|
+
query: params.query,
|
|
1258
|
+
"include-personal-collections": params["include-personal-collections"],
|
|
1259
|
+
"sort-column": params["sort-column"],
|
|
1260
|
+
"sort-direction": params["sort-direction"]
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
//#endregion
|
|
1264
|
+
//#region ../client/src/resources/document.ts
|
|
1265
|
+
const DocumentApiList = z.object({ items: z.array(Document) }).loose();
|
|
1266
|
+
function documentResource(transport) {
|
|
1267
|
+
/** Get existing documents. */
|
|
1268
|
+
async function list(options = {}) {
|
|
1269
|
+
await transport.require("document.list", options);
|
|
1270
|
+
return {
|
|
1271
|
+
data: (await transport.requestParsed(DocumentApiList, "/api/document", { ...options })).items,
|
|
1272
|
+
total: null
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
/** Return an existing document by id. */
|
|
1276
|
+
async function get(id, options = {}) {
|
|
1277
|
+
await transport.require("document.get", options);
|
|
1278
|
+
return transport.requestParsed(Document, `/api/document/${id}`, { ...options });
|
|
1279
|
+
}
|
|
1280
|
+
/** Create a new document. */
|
|
1281
|
+
async function create(params, options = {}) {
|
|
1282
|
+
await transport.require("document.create", options);
|
|
1283
|
+
return transport.requestParsed(Document, "/api/document", {
|
|
1284
|
+
...options,
|
|
1285
|
+
method: "POST",
|
|
1286
|
+
body: params
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
/** Update an existing document by id. */
|
|
1290
|
+
async function update(id, params, options = {}) {
|
|
1291
|
+
await transport.require("document.update", options);
|
|
1292
|
+
return transport.requestParsed(Document, `/api/document/${id}`, {
|
|
1293
|
+
...options,
|
|
1294
|
+
method: "PUT",
|
|
1295
|
+
body: params
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
/** Archive (soft-delete) a document by id. Metabase models this as an update, not its own endpoint. */
|
|
1299
|
+
async function archive(id, options = {}) {
|
|
1300
|
+
await transport.require("document.archive", options);
|
|
1301
|
+
return update(id, { archived: true }, options);
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Copy a document into `collection_id` (root when absent), duplicating the cards saved inside
|
|
1305
|
+
* it. An archived source is not found.
|
|
1306
|
+
*/
|
|
1307
|
+
async function copy(id, params = {}, options = {}) {
|
|
1308
|
+
await transport.require("document.copy", options);
|
|
1309
|
+
return transport.requestParsed(Document, `/api/document/${id}/copy`, {
|
|
1310
|
+
...options,
|
|
1311
|
+
method: "POST",
|
|
1312
|
+
body: params
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
return {
|
|
1316
|
+
list,
|
|
1317
|
+
get,
|
|
1318
|
+
create,
|
|
1319
|
+
update,
|
|
1320
|
+
archive,
|
|
1321
|
+
copy
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
//#endregion
|
|
1325
|
+
//#region ../client/src/resources/eid-translation.ts
|
|
1326
|
+
function eidTranslationResource(transport) {
|
|
1327
|
+
/**
|
|
1328
|
+
* Translate entity ids to the numeric ids of the rows they name. The request groups the entity
|
|
1329
|
+
* ids by model; the response is keyed by entity id, each entry carrying its model and — when the
|
|
1330
|
+
* lookup succeeded — the numeric id.
|
|
1331
|
+
*/
|
|
1332
|
+
async function translate(params, options = {}) {
|
|
1333
|
+
await transport.require("eidTranslation.translate", options);
|
|
1334
|
+
return transport.requestParsed(EidTranslateResult, "/api/eid-translation/translate", {
|
|
1335
|
+
...options,
|
|
1336
|
+
method: "POST",
|
|
1337
|
+
body: params
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
return { translate };
|
|
1341
|
+
}
|
|
1342
|
+
//#endregion
|
|
1343
|
+
//#region ../client/src/domain/erd.ts
|
|
1344
|
+
const ErdField = z.object({
|
|
1345
|
+
id: z.number().int(),
|
|
1346
|
+
name: z.string(),
|
|
1347
|
+
display_name: z.string(),
|
|
1348
|
+
database_type: z.string(),
|
|
1349
|
+
base_type: FieldBaseType.nullable(),
|
|
1350
|
+
effective_type: FieldBaseType.nullable(),
|
|
1351
|
+
semantic_type: FieldSemanticType.nullable(),
|
|
1352
|
+
fk_target_field_id: z.number().int().nullable(),
|
|
1353
|
+
fk_target_table_id: z.number().int().nullable()
|
|
1354
|
+
}).loose();
|
|
1355
|
+
const ErdOwner = z.object({
|
|
1356
|
+
id: z.number().int().optional(),
|
|
1357
|
+
email: z.string(),
|
|
1358
|
+
first_name: z.string().nullable().optional(),
|
|
1359
|
+
last_name: z.string().nullable().optional()
|
|
1360
|
+
}).loose().nullable();
|
|
1361
|
+
const ErdNode = z.object({
|
|
1362
|
+
table_id: z.number().int(),
|
|
1363
|
+
name: z.string(),
|
|
1364
|
+
display_name: z.string(),
|
|
1365
|
+
description: z.string().nullable(),
|
|
1366
|
+
owner: ErdOwner,
|
|
1367
|
+
schema: z.string().nullable(),
|
|
1368
|
+
visibility_type: TableVisibilityType.nullable(),
|
|
1369
|
+
db_id: z.number().int(),
|
|
1370
|
+
fields: z.array(ErdField)
|
|
1371
|
+
}).loose();
|
|
1372
|
+
const ErdEdge = z.object({
|
|
1373
|
+
source_table_id: z.number().int(),
|
|
1374
|
+
source_field_id: z.number().int(),
|
|
1375
|
+
target_table_id: z.number().int(),
|
|
1376
|
+
target_field_id: z.number().int(),
|
|
1377
|
+
relationship: z.enum(["one-to-one", "many-to-one"])
|
|
1378
|
+
}).loose();
|
|
1379
|
+
const Erd = z.object({
|
|
1380
|
+
nodes: z.array(ErdNode),
|
|
1381
|
+
edges: z.array(ErdEdge)
|
|
1382
|
+
}).loose();
|
|
1383
|
+
//#endregion
|
|
1384
|
+
//#region ../client/src/resources/erd.ts
|
|
1385
|
+
const ERD_PATH = "/api/ee/erd";
|
|
1386
|
+
function erdResource(transport) {
|
|
1387
|
+
/**
|
|
1388
|
+
* Get the entity relationship diagram of a database: the readable tables as nodes, each with its
|
|
1389
|
+
* fields, and the foreign keys between them as edges. `table-ids` makes those tables the focal
|
|
1390
|
+
* points and draws their neighbours; `schema` keeps one schema, the empty string the tables with
|
|
1391
|
+
* none.
|
|
1392
|
+
*/
|
|
1393
|
+
async function get(params, options = {}) {
|
|
1394
|
+
await transport.require("erd.get", options);
|
|
1395
|
+
return transport.requestParsed(Erd, ERD_PATH, {
|
|
1396
|
+
...options,
|
|
1397
|
+
query: {
|
|
1398
|
+
"database-id": params["database-id"],
|
|
1399
|
+
"table-ids": params["table-ids"],
|
|
1400
|
+
schema: params.schema
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
return { get };
|
|
1405
|
+
}
|
|
1406
|
+
//#endregion
|
|
1407
|
+
//#region ../client/src/resources/optional-parsed.ts
|
|
1408
|
+
const NO_CONTENT_STATUS = 204;
|
|
1409
|
+
async function fetchOptionalParsed(client, path, schema, options = {}) {
|
|
1410
|
+
const response = await client.requestRaw(path, {
|
|
1411
|
+
...options,
|
|
1412
|
+
method: "GET",
|
|
1413
|
+
expectContentType: "binary"
|
|
1414
|
+
});
|
|
1415
|
+
if (response.status === NO_CONTENT_STATUS) return null;
|
|
1416
|
+
return parseJsonOrPlain(await response.text(), response.headers.get("content-type"), schema, { source: response.url });
|
|
1417
|
+
}
|
|
1418
|
+
//#endregion
|
|
1419
|
+
//#region ../client/src/resources/field.ts
|
|
1420
|
+
const FieldApiSummary = z.tuple([z.tuple([z.literal("count"), z.number().int()]), z.tuple([z.literal("distincts"), z.number().int()])]);
|
|
1421
|
+
function fieldResource(transport) {
|
|
1422
|
+
/** Get one field by id. */
|
|
1423
|
+
async function get(id, options = {}) {
|
|
1424
|
+
await transport.require("field.get", options);
|
|
1425
|
+
return transport.requestParsed(Field, `/api/field/${id}`, { ...options });
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Update a field by id, patching only the fields the body carries. A `data_sensitivity` label is
|
|
1429
|
+
* a person's call the server's classifier never overwrites, and `null` withdraws it so the
|
|
1430
|
+
* classifier's own applies again; a server without the column would drop the key silently, so
|
|
1431
|
+
* the label is refused there before the wire.
|
|
1432
|
+
*/
|
|
1433
|
+
async function update(id, params, options = {}) {
|
|
1434
|
+
await transport.require("field.update", options);
|
|
1435
|
+
await transport.requireFeatures(params.data_sensitivity === void 0 ? [] : ["fieldDataSensitivity"], options);
|
|
1436
|
+
return transport.requestParsed(Field, `/api/field/${id}`, {
|
|
1437
|
+
...options,
|
|
1438
|
+
method: "PUT",
|
|
1439
|
+
body: params
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Search the values of `searchId` that contain `value`, case-insensitively, answering the
|
|
1444
|
+
* matching values of `id` paired with them. An FK on either side is followed to the PK it
|
|
1445
|
+
* points at. Without `value`, the first `limit` values.
|
|
1446
|
+
*/
|
|
1447
|
+
async function search(id, searchId, params, options = {}) {
|
|
1448
|
+
await transport.require("field.search", options);
|
|
1449
|
+
return transport.requestParsed(FieldSearchMatches, `/api/field/${id}/search/${searchId}`, {
|
|
1450
|
+
...options,
|
|
1451
|
+
query: {
|
|
1452
|
+
value: params.value,
|
|
1453
|
+
limit: params.limit
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* The value of `remappedId` on the one row where `id` equals `value`, as `[value, remapped]`,
|
|
1459
|
+
* or `null` when no row matches. `value` is parsed as a number for a numeric field.
|
|
1460
|
+
*/
|
|
1461
|
+
async function remapping(id, remappedId, params, options = {}) {
|
|
1462
|
+
await transport.require("field.remapping", options);
|
|
1463
|
+
return fetchOptionalParsed(transport, `/api/field/${id}/remapping/${remappedId}`, FieldRemappedValue, {
|
|
1464
|
+
...options,
|
|
1465
|
+
query: { value: params.value }
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
/** Get the row count and the distinct-value count for a field. */
|
|
1469
|
+
async function summary(id, options = {}) {
|
|
1470
|
+
await transport.require("field.summary", options);
|
|
1471
|
+
const [[, count], [, distincts]] = await transport.requestParsed(FieldApiSummary, `/api/field/${id}/summary`, { ...options });
|
|
1472
|
+
return {
|
|
1473
|
+
field_id: id,
|
|
1474
|
+
count,
|
|
1475
|
+
distincts
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
/** Get the cached distinct values Metabase holds for a field. */
|
|
1479
|
+
async function values(id, options = {}) {
|
|
1480
|
+
await transport.require("field.values", options);
|
|
1481
|
+
return transport.requestParsed(FieldValues, `/api/field/${id}/values`, { ...options });
|
|
1482
|
+
}
|
|
1483
|
+
return {
|
|
1484
|
+
get,
|
|
1485
|
+
update,
|
|
1486
|
+
search,
|
|
1487
|
+
remapping,
|
|
1488
|
+
summary,
|
|
1489
|
+
values
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
//#endregion
|
|
1493
|
+
//#region ../client/src/resources/git-sync.ts
|
|
1494
|
+
const SyncScopeCollection = Collection.pick({
|
|
1495
|
+
id: true,
|
|
1496
|
+
name: true,
|
|
1497
|
+
is_remote_synced: true
|
|
1498
|
+
}).strip();
|
|
1499
|
+
const SyncDirtyFlag = z.object({ is_dirty: z.boolean() });
|
|
1500
|
+
const SyncDirtyList = z.object({ dirty: z.array(SyncDirtyItem) });
|
|
1501
|
+
const SyncBranchList = z.object({ items: z.array(z.string()) });
|
|
1502
|
+
const SyncImportStarted = z.object({
|
|
1503
|
+
status: z.literal("success"),
|
|
1504
|
+
task_id: z.number().int().positive().nullable(),
|
|
1505
|
+
message: z.string().nullable().optional()
|
|
1506
|
+
});
|
|
1507
|
+
const SyncExportStarted = z.object({
|
|
1508
|
+
message: z.string(),
|
|
1509
|
+
task_id: z.number().int().positive()
|
|
1510
|
+
});
|
|
1511
|
+
const SyncStashStarted = z.object({
|
|
1512
|
+
status: z.literal("success"),
|
|
1513
|
+
message: z.string(),
|
|
1514
|
+
task_id: z.number().int().positive()
|
|
1515
|
+
});
|
|
1516
|
+
const RemoteSyncSetting = z.string().nullable();
|
|
1517
|
+
const FORBIDDEN_STATUS = 403;
|
|
1518
|
+
const UNREGISTERED_STATUS = 404;
|
|
1519
|
+
function isRemoteUnreadable(error) {
|
|
1520
|
+
if (!(error instanceof HttpError)) return false;
|
|
1521
|
+
return error.status === FORBIDDEN_STATUS || error.status === UNREGISTERED_STATUS;
|
|
1522
|
+
}
|
|
1523
|
+
function gitSyncResource(transport) {
|
|
1524
|
+
/** Get the running or most recently finished sync task, or null when the server has none. */
|
|
1525
|
+
async function currentTask(options = {}) {
|
|
1526
|
+
await transport.require("gitSync.currentTask", options);
|
|
1527
|
+
return fetchOptionalParsed(transport, "/api/ee/remote-sync/current-task", SyncTask, options);
|
|
1528
|
+
}
|
|
1529
|
+
/** Request cancellation of the running sync task, and answer it in the state that left it. */
|
|
1530
|
+
async function cancelTask(options = {}) {
|
|
1531
|
+
await transport.require("gitSync.cancelTask", options);
|
|
1532
|
+
return transport.requestParsed(SyncTask, "/api/ee/remote-sync/current-task/cancel", {
|
|
1533
|
+
...options,
|
|
1534
|
+
method: "POST"
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
/** Whether Metabase holds content changes the remote has not been told about. */
|
|
1538
|
+
async function isDirty(options = {}) {
|
|
1539
|
+
await transport.require("gitSync.isDirty", options);
|
|
1540
|
+
return (await transport.requestParsed(SyncDirtyFlag, "/api/ee/remote-sync/is-dirty", { ...options })).is_dirty;
|
|
1541
|
+
}
|
|
1542
|
+
/** List the objects whose local state differs from the remote. */
|
|
1543
|
+
async function dirty(options = {}) {
|
|
1544
|
+
await transport.require("gitSync.dirty", options);
|
|
1545
|
+
return {
|
|
1546
|
+
data: (await transport.requestParsed(SyncDirtyList, "/api/ee/remote-sync/dirty", { ...options })).dirty,
|
|
1547
|
+
total: null
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Compare the tracked remote branch against what Metabase last imported. The server caches the
|
|
1552
|
+
* comparison; `force-refresh` re-reads the remote instead.
|
|
1553
|
+
*/
|
|
1554
|
+
async function hasRemoteChanges(params = {}, options = {}) {
|
|
1555
|
+
await transport.require("gitSync.hasRemoteChanges", options);
|
|
1556
|
+
return transport.requestParsed(SyncRemoteChanges, "/api/ee/remote-sync/has-remote-changes", {
|
|
1557
|
+
...options,
|
|
1558
|
+
query: { "force-refresh": params["force-refresh"] }
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Import content from the remote into Metabase. The endpoint queues a task and returns at once;
|
|
1563
|
+
* pass `wait` to poll that task until it reaches a terminal status. A server already up to date
|
|
1564
|
+
* answers no task id, and there is then nothing to poll.
|
|
1565
|
+
*/
|
|
1566
|
+
async function importFromRemote(params = {}, options = {}) {
|
|
1567
|
+
await transport.require("gitSync.import", options);
|
|
1568
|
+
const started = await transport.requestParsed(SyncImportStarted, "/api/ee/remote-sync/import", {
|
|
1569
|
+
...options,
|
|
1570
|
+
method: "POST",
|
|
1571
|
+
body: {
|
|
1572
|
+
branch: params.branch,
|
|
1573
|
+
force: params.force
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
const message = started.message ?? null;
|
|
1577
|
+
if (params.wait === void 0 || started.task_id === null) return {
|
|
1578
|
+
message,
|
|
1579
|
+
task_id: started.task_id
|
|
1580
|
+
};
|
|
1581
|
+
return {
|
|
1582
|
+
message,
|
|
1583
|
+
task_id: started.task_id,
|
|
1584
|
+
final: await settle(params.wait, options)
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Export Metabase's content to the remote. The endpoint queues a task and returns at once; pass
|
|
1589
|
+
* `wait` to poll that task until it reaches a terminal status.
|
|
1590
|
+
*/
|
|
1591
|
+
async function exportToRemote(params = {}, options = {}) {
|
|
1592
|
+
await transport.require("gitSync.export", options);
|
|
1593
|
+
const started = await transport.requestParsed(SyncExportStarted, "/api/ee/remote-sync/export", {
|
|
1594
|
+
...options,
|
|
1595
|
+
method: "POST",
|
|
1596
|
+
body: {
|
|
1597
|
+
branch: params.branch,
|
|
1598
|
+
message: params.message,
|
|
1599
|
+
force: params.force
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
if (params.wait === void 0) return {
|
|
1603
|
+
message: started.message,
|
|
1604
|
+
task_id: started.task_id
|
|
1605
|
+
};
|
|
1606
|
+
return {
|
|
1607
|
+
message: started.message,
|
|
1608
|
+
task_id: started.task_id,
|
|
1609
|
+
final: await settle(params.wait, options)
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Preview what exporting to `branch` would do against the live remote, without writing: whether
|
|
1614
|
+
* the remote has moved on, whether a merge would apply cleanly, which entities conflict, and what
|
|
1615
|
+
* a force push would discard. `branch` must be the one git-sync tracks; the server answers 409
|
|
1616
|
+
* otherwise.
|
|
1617
|
+
*/
|
|
1618
|
+
async function exportPreflight(params, options = {}) {
|
|
1619
|
+
await transport.require("gitSync.exportPreflight", options);
|
|
1620
|
+
return transport.requestParsed(SyncExportPreflight, "/api/ee/remote-sync/export-preflight", {
|
|
1621
|
+
...options,
|
|
1622
|
+
query: { branch: params.branch }
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Export Metabase's current content to a branch the remote does not have yet. The endpoint
|
|
1627
|
+
* queues a task and returns at once; pass `wait` to poll that task to a terminal status.
|
|
1628
|
+
*/
|
|
1629
|
+
async function stash(params, options = {}) {
|
|
1630
|
+
await transport.require("gitSync.stash", options);
|
|
1631
|
+
const started = await transport.requestParsed(SyncStashStarted, "/api/ee/remote-sync/stash", {
|
|
1632
|
+
...options,
|
|
1633
|
+
method: "POST",
|
|
1634
|
+
body: {
|
|
1635
|
+
new_branch: params.new_branch,
|
|
1636
|
+
message: params.message
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
if (params.wait === void 0) return {
|
|
1640
|
+
status: started.status,
|
|
1641
|
+
message: started.message,
|
|
1642
|
+
task_id: started.task_id
|
|
1643
|
+
};
|
|
1644
|
+
return {
|
|
1645
|
+
status: started.status,
|
|
1646
|
+
message: started.message,
|
|
1647
|
+
task_id: started.task_id,
|
|
1648
|
+
final: await settle(params.wait, options)
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
/** List the branches the configured remote carries. */
|
|
1652
|
+
async function branches(options = {}) {
|
|
1653
|
+
await transport.require("gitSync.branches", options);
|
|
1654
|
+
return {
|
|
1655
|
+
data: (await transport.requestParsed(SyncBranchList, "/api/ee/remote-sync/branches", { ...options })).items,
|
|
1656
|
+
total: null
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
/** Create a branch on the remote and point git-sync at it. */
|
|
1660
|
+
async function createBranch(params, options = {}) {
|
|
1661
|
+
await transport.require("gitSync.createBranch", options);
|
|
1662
|
+
return transport.requestParsed(SyncBranchCreated, "/api/ee/remote-sync/create-branch", {
|
|
1663
|
+
...options,
|
|
1664
|
+
method: "POST",
|
|
1665
|
+
body: { name: params.name }
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Mark one collection as git-synced, or unmark it. The server cascades the flag to descendants
|
|
1670
|
+
* by location prefix, and may queue an export task to carry the change to the remote.
|
|
1671
|
+
*/
|
|
1672
|
+
async function setCollectionSynced(collectionId, synced, options = {}) {
|
|
1673
|
+
await transport.require("gitSync.setCollectionSynced", options);
|
|
1674
|
+
return transport.requestParsed(SyncSettingsUpdateResult, "/api/ee/remote-sync/settings", {
|
|
1675
|
+
...options,
|
|
1676
|
+
method: "PUT",
|
|
1677
|
+
body: { collections: { [collectionId]: synced } }
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
/** The collections currently in git-sync's scope. */
|
|
1681
|
+
async function syncedCollections(options = {}) {
|
|
1682
|
+
await transport.require("gitSync.syncedCollections", options);
|
|
1683
|
+
return {
|
|
1684
|
+
data: (await listCollectionsWithLibrary(transport, SyncScopeCollection, options)).filter((entry) => entry.is_remote_synced === true),
|
|
1685
|
+
total: null
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
/** The remote's URL, or null when none is configured or the caller may not read it. */
|
|
1689
|
+
async function remoteUrl(options = {}) {
|
|
1690
|
+
await transport.require("gitSync.remoteUrl", options);
|
|
1691
|
+
try {
|
|
1692
|
+
const url = await fetchOptionalParsed(transport, "/api/setting/remote-sync-url", RemoteSyncSetting, options);
|
|
1693
|
+
return url === "" ? null : url;
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
if (isRemoteUnreadable(error)) return null;
|
|
1696
|
+
throw error;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* The branch git-sync tracks, or null when none is configured, the caller may not read it, or
|
|
1701
|
+
* the server has no remote-sync module.
|
|
1702
|
+
*/
|
|
1703
|
+
async function branch(options = {}) {
|
|
1704
|
+
await transport.require("gitSync.branch", options);
|
|
1705
|
+
try {
|
|
1706
|
+
return await fetchOptionalParsed(transport, "/api/setting/remote-sync-branch", RemoteSyncSetting, options);
|
|
1707
|
+
} catch (error) {
|
|
1708
|
+
if (isRemoteUnreadable(error)) return null;
|
|
1709
|
+
throw error;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Poll the current task until it reaches a terminal status. A server with no task at all
|
|
1714
|
+
* answers null, which is terminal in the same sense: nothing further will happen.
|
|
1715
|
+
*/
|
|
1716
|
+
async function waitForTask(wait, options = {}) {
|
|
1717
|
+
await transport.require("gitSync.waitForTask", options);
|
|
1718
|
+
return settle(wait, options);
|
|
1719
|
+
}
|
|
1720
|
+
async function settle(wait, options) {
|
|
1721
|
+
return pollUntil(async (signal) => currentTask({
|
|
1722
|
+
...options,
|
|
1723
|
+
signal
|
|
1724
|
+
}), (task) => task === null || isSyncTaskTerminal(task.status), wait);
|
|
1725
|
+
}
|
|
1726
|
+
return {
|
|
1727
|
+
currentTask,
|
|
1728
|
+
cancelTask,
|
|
1729
|
+
isDirty,
|
|
1730
|
+
dirty,
|
|
1731
|
+
hasRemoteChanges,
|
|
1732
|
+
import: importFromRemote,
|
|
1733
|
+
export: exportToRemote,
|
|
1734
|
+
exportPreflight,
|
|
1735
|
+
stash,
|
|
1736
|
+
branches,
|
|
1737
|
+
createBranch,
|
|
1738
|
+
setCollectionSynced,
|
|
1739
|
+
syncedCollections,
|
|
1740
|
+
remoteUrl,
|
|
1741
|
+
branch,
|
|
1742
|
+
waitForTask
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
//#endregion
|
|
1746
|
+
//#region ../client/src/domain/glossary.ts
|
|
1747
|
+
const GlossaryCreator = z.object({
|
|
1748
|
+
id: z.number().int(),
|
|
1749
|
+
email: z.string(),
|
|
1750
|
+
first_name: z.string().nullable(),
|
|
1751
|
+
last_name: z.string().nullable()
|
|
1752
|
+
}).loose();
|
|
1753
|
+
const Glossary = z.object({
|
|
1754
|
+
id: z.number().int(),
|
|
1755
|
+
term: z.string(),
|
|
1756
|
+
definition: z.string(),
|
|
1757
|
+
creator_id: z.number().int(),
|
|
1758
|
+
creator: GlossaryCreator,
|
|
1759
|
+
created_at: z.string(),
|
|
1760
|
+
updated_at: z.string()
|
|
1761
|
+
}).loose();
|
|
1762
|
+
Glossary.pick({
|
|
1763
|
+
id: true,
|
|
1764
|
+
term: true,
|
|
1765
|
+
definition: true
|
|
1766
|
+
}).strip();
|
|
1767
|
+
z.object({
|
|
1768
|
+
term: z.string().min(1),
|
|
1769
|
+
definition: z.string().min(1)
|
|
1770
|
+
}).strict();
|
|
1771
|
+
//#endregion
|
|
1772
|
+
//#region ../client/src/resources/glossary.ts
|
|
1773
|
+
const GlossaryApiList = z.object({ data: z.array(Glossary) }).loose();
|
|
1774
|
+
function glossaryResource(transport) {
|
|
1775
|
+
/**
|
|
1776
|
+
* List glossary entries in term order. `search` keeps the entries whose term or definition
|
|
1777
|
+
* contains it, case-insensitively.
|
|
1778
|
+
*/
|
|
1779
|
+
async function list(params = {}, options = {}) {
|
|
1780
|
+
await transport.require("glossary.list", options);
|
|
1781
|
+
return {
|
|
1782
|
+
data: (await transport.requestParsed(GlossaryApiList, "/api/glossary", {
|
|
1783
|
+
...options,
|
|
1784
|
+
query: { search: params.search }
|
|
1785
|
+
})).data,
|
|
1786
|
+
total: null
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
/** Create a glossary entry, a term and its definition. Terms are unique. */
|
|
1790
|
+
async function create(params, options = {}) {
|
|
1791
|
+
await transport.require("glossary.create", options);
|
|
1792
|
+
return transport.requestParsed(Glossary, "/api/glossary", {
|
|
1793
|
+
...options,
|
|
1794
|
+
method: "POST",
|
|
1795
|
+
body: params
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
/** Replace the term and definition of a glossary entry by id. */
|
|
1799
|
+
async function update(id, params, options = {}) {
|
|
1800
|
+
await transport.require("glossary.update", options);
|
|
1801
|
+
return transport.requestParsed(Glossary, `/api/glossary/${id}`, {
|
|
1802
|
+
...options,
|
|
1803
|
+
method: "PUT",
|
|
1804
|
+
body: params
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
/** Delete a glossary entry by id. */
|
|
1808
|
+
async function remove(id, options = {}) {
|
|
1809
|
+
await transport.require("glossary.delete", options);
|
|
1810
|
+
await transport.requestRaw(`/api/glossary/${id}`, {
|
|
1811
|
+
...options,
|
|
1812
|
+
method: "DELETE",
|
|
1813
|
+
expectContentType: "binary"
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
return {
|
|
1817
|
+
list,
|
|
1818
|
+
create,
|
|
1819
|
+
update,
|
|
1820
|
+
delete: remove
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
//#endregion
|
|
1824
|
+
//#region ../client/src/resources/library.ts
|
|
1825
|
+
const LIBRARY_ROOT_PATH = "/api/ee/library/";
|
|
1826
|
+
const PUBLISH_TABLES_PATH = "/api/ee/data-studio/table/publish-tables";
|
|
1827
|
+
const UNPUBLISH_TABLES_PATH = "/api/ee/data-studio/table/unpublish-tables";
|
|
1828
|
+
const LIBRARY_DATA_TYPE = "library-data";
|
|
1829
|
+
const PublishTablesResponse = z.object({ target_collection: Collection.nullable() });
|
|
1830
|
+
function libraryResource(transport) {
|
|
1831
|
+
/** Get the Library root and its child collections, or `null` on an instance that has none. */
|
|
1832
|
+
async function get(options = {}) {
|
|
1833
|
+
await transport.require("library.get", options);
|
|
1834
|
+
const { features } = await transport.server(options);
|
|
1835
|
+
const wire = await transport.requestParsed(libraryWireSchema(features), LIBRARY_ROOT_PATH, { ...options });
|
|
1836
|
+
if (wire === null) return null;
|
|
1837
|
+
return toLibrary(wire, await libraryListing(options));
|
|
1838
|
+
}
|
|
1839
|
+
async function libraryListing(options) {
|
|
1840
|
+
const data = await listCollectionsWithLibrary(transport, LibraryCollectionInfo, options);
|
|
1841
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1842
|
+
for (const collection of data) if (typeof collection.id === "number") byId.set(collection.id, collection);
|
|
1843
|
+
return byId;
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Create the Library subtree (its Data and Metrics collections). Metabase takes no body, rejects
|
|
1847
|
+
* a second create with a 400, and answers the bare root collection without its
|
|
1848
|
+
* `effective_children` — so an existing Library short-circuits the call and a fresh one comes
|
|
1849
|
+
* back from a refetch, which together make this idempotent.
|
|
1850
|
+
*/
|
|
1851
|
+
async function create(options = {}) {
|
|
1852
|
+
await transport.require("library.create", options);
|
|
1853
|
+
const existing = await get(options);
|
|
1854
|
+
if (existing !== null) return existing;
|
|
1855
|
+
await transport.requestRaw(LIBRARY_ROOT_PATH, {
|
|
1856
|
+
...options,
|
|
1857
|
+
method: "POST"
|
|
1858
|
+
});
|
|
1859
|
+
const created = await get(options);
|
|
1860
|
+
if (created === null) throw new Error(`Library was not created after POST ${LIBRARY_ROOT_PATH}`);
|
|
1861
|
+
return created;
|
|
1862
|
+
}
|
|
1863
|
+
/** The id of the Library's Data collection, creating the Library first when it does not exist. */
|
|
1864
|
+
async function ensureDataCollectionId(options = {}) {
|
|
1865
|
+
await transport.require("library.ensureDataCollectionId", options);
|
|
1866
|
+
const data = (await create(options)).effective_children.find((child) => child.type === LIBRARY_DATA_TYPE);
|
|
1867
|
+
if (data === void 0) throw new Error("Library has no Data collection to publish into");
|
|
1868
|
+
if (typeof data.id !== "number") throw new Error(`Library Data collection has a non-numeric id ${String(data.id)}`);
|
|
1869
|
+
return data.id;
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Publish tables — and every upstream table they depend on — into a collection, so they lead the
|
|
1873
|
+
* data pickers and rank up in search. Answers the collection they landed in.
|
|
1874
|
+
*/
|
|
1875
|
+
async function publishTables(params, options = {}) {
|
|
1876
|
+
await transport.require("library.publishTables", options);
|
|
1877
|
+
return (await transport.requestParsed(PublishTablesResponse, PUBLISH_TABLES_PATH, {
|
|
1878
|
+
...options,
|
|
1879
|
+
method: "POST",
|
|
1880
|
+
body: params
|
|
1881
|
+
})).target_collection;
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Clear the Library collection from tables, and recursively from every downstream table that
|
|
1885
|
+
* depends on them. The endpoint answers no JSON body.
|
|
1886
|
+
*/
|
|
1887
|
+
async function unpublishTables(params, options = {}) {
|
|
1888
|
+
await transport.require("library.unpublishTables", options);
|
|
1889
|
+
await transport.requestRaw(UNPUBLISH_TABLES_PATH, {
|
|
1890
|
+
...options,
|
|
1891
|
+
method: "POST",
|
|
1892
|
+
body: params,
|
|
1893
|
+
expectContentType: "binary"
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
return {
|
|
1897
|
+
get,
|
|
1898
|
+
create,
|
|
1899
|
+
ensureDataCollectionId,
|
|
1900
|
+
publishTables,
|
|
1901
|
+
unpublishTables
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
//#endregion
|
|
1905
|
+
//#region ../client/src/resources/measure.ts
|
|
1906
|
+
const MeasureApiList = z.array(Measure);
|
|
1907
|
+
function measureResource(transport) {
|
|
1908
|
+
/** List measures. */
|
|
1909
|
+
async function list(options = {}) {
|
|
1910
|
+
await transport.require("measure.list", options);
|
|
1911
|
+
return {
|
|
1912
|
+
data: await transport.requestParsed(MeasureApiList, "/api/measure", { ...options }),
|
|
1913
|
+
total: null
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
/** Get one measure by id. */
|
|
1917
|
+
async function get(id, options = {}) {
|
|
1918
|
+
await transport.require("measure.get", options);
|
|
1919
|
+
return transport.requestParsed(Measure, `/api/measure/${id}`, { ...options });
|
|
1920
|
+
}
|
|
1921
|
+
/** Create a measure — a saved aggregation tied to a table — from a full measure body. */
|
|
1922
|
+
async function create(params, options = {}) {
|
|
1923
|
+
await transport.require("measure.create", options);
|
|
1924
|
+
return transport.requestParsed(Measure, "/api/measure", {
|
|
1925
|
+
...options,
|
|
1926
|
+
method: "POST",
|
|
1927
|
+
body: params
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Update a measure by id, patching only the fields the body carries. `revision_message` is
|
|
1932
|
+
* required and lands in the audit log.
|
|
1933
|
+
*/
|
|
1934
|
+
async function update(id, params, options = {}) {
|
|
1935
|
+
await transport.require("measure.update", options);
|
|
1936
|
+
return transport.requestParsed(Measure, `/api/measure/${id}`, {
|
|
1937
|
+
...options,
|
|
1938
|
+
method: "PUT",
|
|
1939
|
+
body: params
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Archive (soft-delete) a measure by id. Metabase models this as an update, not its own endpoint,
|
|
1944
|
+
* so it carries the same required `revision_message`.
|
|
1945
|
+
*/
|
|
1946
|
+
async function archive(id, params, options = {}) {
|
|
1947
|
+
await transport.require("measure.archive", options);
|
|
1948
|
+
return update(id, {
|
|
1949
|
+
archived: true,
|
|
1950
|
+
revision_message: params.revision_message
|
|
1951
|
+
}, options);
|
|
1952
|
+
}
|
|
1953
|
+
return {
|
|
1954
|
+
list,
|
|
1955
|
+
get,
|
|
1956
|
+
create,
|
|
1957
|
+
update,
|
|
1958
|
+
archive
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
//#endregion
|
|
1962
|
+
//#region ../client/src/domain/metric.ts
|
|
1963
|
+
const ClauseOptions = z.object({}).loose();
|
|
1964
|
+
const MetricLeafType = z.enum(["metric", "measure"]);
|
|
1965
|
+
const MetricLeafRef = z.tuple([
|
|
1966
|
+
MetricLeafType,
|
|
1967
|
+
z.object({ "lib/uuid": z.string().min(1) }).strict(),
|
|
1968
|
+
z.number().int().positive()
|
|
1969
|
+
]);
|
|
1970
|
+
const MetricMathOperator = z.enum([
|
|
1971
|
+
"+",
|
|
1972
|
+
"-",
|
|
1973
|
+
"*",
|
|
1974
|
+
"/"
|
|
1975
|
+
]);
|
|
1976
|
+
const MetricMathExpression = z.lazy(() => z.union([
|
|
1977
|
+
MetricLeafRef,
|
|
1978
|
+
z.number(),
|
|
1979
|
+
z.tuple([
|
|
1980
|
+
MetricMathOperator,
|
|
1981
|
+
ClauseOptions,
|
|
1982
|
+
MetricMathExpression,
|
|
1983
|
+
MetricMathExpression
|
|
1984
|
+
]).rest(MetricMathExpression)
|
|
1985
|
+
]));
|
|
1986
|
+
const MetricBinning = z.discriminatedUnion("strategy", [
|
|
1987
|
+
z.object({ strategy: z.literal("default") }).strict(),
|
|
1988
|
+
z.object({
|
|
1989
|
+
strategy: z.literal("bin-width"),
|
|
1990
|
+
"bin-width": z.number().positive()
|
|
1991
|
+
}).strict(),
|
|
1992
|
+
z.object({
|
|
1993
|
+
strategy: z.literal("num-bins"),
|
|
1994
|
+
"num-bins": z.number().int().positive()
|
|
1995
|
+
}).strict()
|
|
1996
|
+
]);
|
|
1997
|
+
const MetricDimensionRefOptions = z.object({
|
|
1998
|
+
"lib/uuid": z.string().min(1).optional(),
|
|
1999
|
+
"display-name": z.string().min(1).nullable().optional(),
|
|
2000
|
+
"effective-type": FieldBaseType.nullable().optional(),
|
|
2001
|
+
"semantic-type": FieldSemanticType.nullable().optional(),
|
|
2002
|
+
"temporal-unit": TemporalUnit.nullable().optional(),
|
|
2003
|
+
binning: MetricBinning.nullable().optional()
|
|
2004
|
+
}).strict();
|
|
2005
|
+
const MetricDimensionRef = z.tuple([
|
|
2006
|
+
z.literal("dimension"),
|
|
2007
|
+
MetricDimensionRefOptions,
|
|
2008
|
+
z.string().min(1)
|
|
2009
|
+
]);
|
|
2010
|
+
const MetricFilterArgument = z.lazy(() => z.union([
|
|
2011
|
+
MetricDimensionRef,
|
|
2012
|
+
MetricFilterClause,
|
|
2013
|
+
z.string(),
|
|
2014
|
+
z.number(),
|
|
2015
|
+
z.boolean(),
|
|
2016
|
+
z.null()
|
|
2017
|
+
]));
|
|
2018
|
+
const MetricFilterClause = z.lazy(() => z.tuple([z.string().min(1), ClauseOptions]).rest(MetricFilterArgument));
|
|
2019
|
+
const MetricInstanceFilter = z.object({
|
|
2020
|
+
"lib/uuid": z.string().min(1),
|
|
2021
|
+
filter: MetricFilterClause
|
|
2022
|
+
}).strict();
|
|
2023
|
+
const MetricProjection = z.object({
|
|
2024
|
+
type: MetricLeafType,
|
|
2025
|
+
id: z.number().int().positive(),
|
|
2026
|
+
"lib/uuid": z.string().min(1),
|
|
2027
|
+
projection: z.array(MetricDimensionRef)
|
|
2028
|
+
}).strict();
|
|
2029
|
+
z.object({
|
|
2030
|
+
expression: MetricMathExpression,
|
|
2031
|
+
filters: z.array(MetricInstanceFilter).nullable().optional(),
|
|
2032
|
+
projections: z.array(MetricProjection).nullable().optional()
|
|
2033
|
+
}).strict();
|
|
2034
|
+
const MetricBreakoutColumn = z.object({
|
|
2035
|
+
name: z.string().optional(),
|
|
2036
|
+
display_name: z.string().optional(),
|
|
2037
|
+
base_type: FieldBaseType.optional(),
|
|
2038
|
+
effective_type: FieldBaseType.optional(),
|
|
2039
|
+
semantic_type: FieldSemanticType.nullable().optional()
|
|
2040
|
+
}).loose();
|
|
2041
|
+
const MetricBreakoutValues = z.object({
|
|
2042
|
+
values: z.array(z.unknown()),
|
|
2043
|
+
col: MetricBreakoutColumn
|
|
2044
|
+
}).loose();
|
|
2045
|
+
const MetricDimensionStatus = z.enum(["status/active", "status/orphaned"]);
|
|
2046
|
+
const MetricDimensionSource = z.object({
|
|
2047
|
+
type: z.literal("field"),
|
|
2048
|
+
"field-id": z.number().int().nullable()
|
|
2049
|
+
}).loose();
|
|
2050
|
+
const MetricDimensionGroup = z.object({
|
|
2051
|
+
id: z.string(),
|
|
2052
|
+
type: z.enum(["main", "connection"]),
|
|
2053
|
+
display_name: z.string()
|
|
2054
|
+
}).loose();
|
|
2055
|
+
const MetricDimension = z.object({
|
|
2056
|
+
id: z.string(),
|
|
2057
|
+
name: z.string().nullable(),
|
|
2058
|
+
display_name: z.string().nullable(),
|
|
2059
|
+
description: z.string().nullable().optional(),
|
|
2060
|
+
effective_type: FieldBaseType.nullable(),
|
|
2061
|
+
semantic_type: FieldSemanticType.nullable(),
|
|
2062
|
+
has_field_values: z.enum([
|
|
2063
|
+
"list",
|
|
2064
|
+
"search",
|
|
2065
|
+
"none"
|
|
2066
|
+
]).optional(),
|
|
2067
|
+
status: MetricDimensionStatus.optional(),
|
|
2068
|
+
status_message: z.string().optional(),
|
|
2069
|
+
sources: z.array(MetricDimensionSource).optional(),
|
|
2070
|
+
group: MetricDimensionGroup.optional(),
|
|
2071
|
+
dimension_interestingness: z.number().optional(),
|
|
2072
|
+
default_temporal_unit: TemporalUnit.optional(),
|
|
2073
|
+
default: z.boolean().nullable().optional()
|
|
2074
|
+
}).loose();
|
|
2075
|
+
MetricDimension.pick({
|
|
2076
|
+
id: true,
|
|
2077
|
+
name: true,
|
|
2078
|
+
display_name: true,
|
|
2079
|
+
description: true,
|
|
2080
|
+
effective_type: true,
|
|
2081
|
+
semantic_type: true,
|
|
2082
|
+
status: true,
|
|
2083
|
+
group: true,
|
|
2084
|
+
default: true
|
|
2085
|
+
}).strip();
|
|
2086
|
+
const MetricMappingTarget = z.tuple([z.string(), ClauseOptions]).rest(z.unknown());
|
|
2087
|
+
const MetricAddableDimension = MetricDimension.extend({ mapping_target: MetricMappingTarget });
|
|
2088
|
+
const MetricAddableGroup = z.object({
|
|
2089
|
+
group: MetricDimensionGroup.nullable(),
|
|
2090
|
+
dimensions: z.array(MetricAddableDimension)
|
|
2091
|
+
}).loose();
|
|
2092
|
+
const MetricDimensionListing = z.object({
|
|
2093
|
+
added: z.array(MetricDimension),
|
|
2094
|
+
addable: z.array(MetricAddableGroup)
|
|
2095
|
+
}).loose();
|
|
2096
|
+
//#endregion
|
|
2097
|
+
//#region ../client/src/resources/metric.ts
|
|
2098
|
+
function metricResource(transport) {
|
|
2099
|
+
/**
|
|
2100
|
+
* Run a metric definition and return the query result envelope: one metric or measure, or an
|
|
2101
|
+
* arithmetic expression over several, each leaf filtered and projected on its own `lib/uuid`.
|
|
2102
|
+
*/
|
|
2103
|
+
async function query(definition, options = {}) {
|
|
2104
|
+
await transport.require("metric.query", options);
|
|
2105
|
+
return transport.requestParsed(CardQueryResult, "/api/metric/dataset", {
|
|
2106
|
+
...options,
|
|
2107
|
+
method: "POST",
|
|
2108
|
+
body: { definition }
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Fetch the distinct values of a definition's breakout dimension, with that column's metadata.
|
|
2113
|
+
* Takes the same definition `query` does.
|
|
2114
|
+
*/
|
|
2115
|
+
async function breakoutValues(definition, options = {}) {
|
|
2116
|
+
await transport.require("metric.breakoutValues", options);
|
|
2117
|
+
return transport.requestParsed(MetricBreakoutValues, "/api/metric/breakout-values", {
|
|
2118
|
+
...options,
|
|
2119
|
+
method: "POST",
|
|
2120
|
+
body: { definition }
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* List a metric's curated dimensions and, under `with-addable`, the columns still available to
|
|
2125
|
+
* add, grouped by source table. `query` filters both by a name substring.
|
|
2126
|
+
*/
|
|
2127
|
+
async function dimensions(id, params = {}, options = {}) {
|
|
2128
|
+
await transport.require("metric.dimensions", options);
|
|
2129
|
+
return transport.requestParsed(MetricDimensionListing, `/api/metric/${id}/dimension`, {
|
|
2130
|
+
...options,
|
|
2131
|
+
query: {
|
|
2132
|
+
query: params.query,
|
|
2133
|
+
"with-addable": params["with-addable"],
|
|
2134
|
+
"include-orphaned": params["include-orphaned"]
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
return {
|
|
2139
|
+
query,
|
|
2140
|
+
breakoutValues,
|
|
2141
|
+
dimensions
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
//#endregion
|
|
2145
|
+
//#region ../client/src/domain/moderation-review.ts
|
|
2146
|
+
const ModeratedItemType = z.enum(["card", "dashboard"]);
|
|
2147
|
+
const ModerationStatus = z.enum(["verified"]);
|
|
2148
|
+
const ModerationReview = z.object({
|
|
2149
|
+
id: z.number().int(),
|
|
2150
|
+
moderated_item_id: z.number().int(),
|
|
2151
|
+
moderated_item_type: ModeratedItemType,
|
|
2152
|
+
moderator_id: z.number().int(),
|
|
2153
|
+
status: ModerationStatus.nullable(),
|
|
2154
|
+
text: z.string().nullable(),
|
|
2155
|
+
most_recent: z.boolean(),
|
|
2156
|
+
created_at: z.string(),
|
|
2157
|
+
updated_at: z.string()
|
|
2158
|
+
}).loose();
|
|
2159
|
+
ModerationReview.pick({
|
|
2160
|
+
id: true,
|
|
2161
|
+
moderated_item_id: true,
|
|
2162
|
+
moderated_item_type: true,
|
|
2163
|
+
status: true,
|
|
2164
|
+
text: true,
|
|
2165
|
+
most_recent: true
|
|
2166
|
+
}).strip();
|
|
2167
|
+
z.object({
|
|
2168
|
+
moderated_item_id: z.number().int().positive(),
|
|
2169
|
+
moderated_item_type: ModeratedItemType,
|
|
2170
|
+
status: ModerationStatus.nullable().optional(),
|
|
2171
|
+
text: z.string().nullable().optional()
|
|
2172
|
+
}).strict();
|
|
2173
|
+
//#endregion
|
|
2174
|
+
//#region ../client/src/resources/moderation-review.ts
|
|
2175
|
+
function moderationReviewResource(transport) {
|
|
2176
|
+
/**
|
|
2177
|
+
* Create a moderation review on a card or dashboard. `status: "verified"` marks the item
|
|
2178
|
+
* verified, `null` or an absent status records a note that leaves it unverified. The new review
|
|
2179
|
+
* becomes the item's most recent one; the server keeps the ten newest per item. Admins only.
|
|
2180
|
+
*/
|
|
2181
|
+
async function create(params, options = {}) {
|
|
2182
|
+
await transport.require("moderationReview.create", options);
|
|
2183
|
+
return transport.requestParsed(ModerationReview, "/api/moderation-review", {
|
|
2184
|
+
...options,
|
|
2185
|
+
method: "POST",
|
|
2186
|
+
body: params
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
return { create };
|
|
2190
|
+
}
|
|
2191
|
+
//#endregion
|
|
2192
|
+
//#region ../client/src/resources/notification.ts
|
|
2193
|
+
const NotificationApiList = z.array(Notification);
|
|
2194
|
+
function assertCardAlert(notification) {
|
|
2195
|
+
if (notification.payload_type !== "notification/card") throw new ConfigError(`notification ${notification.id} is a ${notification.payload_type}, not a question alert — this operation accepts card alerts only`);
|
|
2196
|
+
return notification;
|
|
2197
|
+
}
|
|
2198
|
+
function mergeAlertUpdate(current, patch) {
|
|
2199
|
+
return Notification.parse({
|
|
2200
|
+
...current,
|
|
2201
|
+
...patch,
|
|
2202
|
+
id: current.id,
|
|
2203
|
+
payload: patch.payload === void 0 ? current.payload : mergePayload(current, patch.payload)
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
function mergePayload(current, patch) {
|
|
2207
|
+
if (current.payload === null) throw new ConfigError(`alert ${current.id} has lost its card payload — it can be archived, but not patched`);
|
|
2208
|
+
return NotificationCardPayload.parse({
|
|
2209
|
+
...current.payload,
|
|
2210
|
+
...patch
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
function notificationResource(transport) {
|
|
2214
|
+
/**
|
|
2215
|
+
* List question alerts, optionally narrowed by the card they watch, their creator, or a
|
|
2216
|
+
* recipient. `/api/notification` also serves Metabase's internal system-event notifications, so
|
|
2217
|
+
* the request pins the card payload type.
|
|
2218
|
+
*/
|
|
2219
|
+
async function list(params = {}, options = {}) {
|
|
2220
|
+
await transport.require("notification.list", options);
|
|
2221
|
+
return {
|
|
2222
|
+
data: await transport.requestParsed(NotificationApiList, "/api/notification", {
|
|
2223
|
+
...options,
|
|
2224
|
+
query: {
|
|
2225
|
+
payload_type: CARD_PAYLOAD_TYPE,
|
|
2226
|
+
card_id: params.card_id,
|
|
2227
|
+
creator_id: params.creator_id,
|
|
2228
|
+
recipient_id: params.recipient_id,
|
|
2229
|
+
include_inactive: params.include_inactive
|
|
2230
|
+
}
|
|
2231
|
+
}),
|
|
2232
|
+
total: null
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
/** Get one question alert by id, refusing an id that names a system-event notification. */
|
|
2236
|
+
async function get(id, options = {}) {
|
|
2237
|
+
await transport.require("notification.get", options);
|
|
2238
|
+
return assertCardAlert(await transport.requestParsed(Notification, `/api/notification/${id}`, { ...options }));
|
|
2239
|
+
}
|
|
2240
|
+
/** Create a question alert — its card payload, schedules and handlers — from a full body. */
|
|
2241
|
+
async function create(params, options = {}) {
|
|
2242
|
+
await transport.require("notification.create", options);
|
|
2243
|
+
return transport.requestParsed(Notification, "/api/notification", {
|
|
2244
|
+
...options,
|
|
2245
|
+
method: "POST",
|
|
2246
|
+
body: params
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
/** Update a question alert by id, merging the patch over the stored notification. */
|
|
2250
|
+
async function update(id, params, options = {}) {
|
|
2251
|
+
await transport.require("notification.update", options);
|
|
2252
|
+
const current = await get(id, options);
|
|
2253
|
+
return transport.requestParsed(Notification, `/api/notification/${id}`, {
|
|
2254
|
+
...options,
|
|
2255
|
+
method: "PUT",
|
|
2256
|
+
body: mergeAlertUpdate(current, params)
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
/** Archive a question alert by id, stopping every delivery. Metabase models this as an update. */
|
|
2260
|
+
async function archive(id, options = {}) {
|
|
2261
|
+
await transport.require("notification.archive", options);
|
|
2262
|
+
return update(id, { active: false }, options);
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Send a question alert now, off-schedule, to every handler on it. The server answers with no
|
|
2266
|
+
* body.
|
|
2267
|
+
*/
|
|
2268
|
+
async function send(id, options = {}) {
|
|
2269
|
+
await transport.require("notification.send", options);
|
|
2270
|
+
await get(id, options);
|
|
2271
|
+
await transport.requestRaw(`/api/notification/${id}/send`, {
|
|
2272
|
+
...options,
|
|
2273
|
+
method: "POST",
|
|
2274
|
+
expectContentType: "binary"
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
return {
|
|
2278
|
+
list,
|
|
2279
|
+
get,
|
|
2280
|
+
create,
|
|
2281
|
+
update,
|
|
2282
|
+
archive,
|
|
2283
|
+
send
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
//#endregion
|
|
2287
|
+
//#region ../client/src/domain/permission.ts
|
|
2288
|
+
const ViewDataPermission = z.enum([
|
|
2289
|
+
"unrestricted",
|
|
2290
|
+
"legacy-no-self-service",
|
|
2291
|
+
"blocked",
|
|
2292
|
+
"sandboxed",
|
|
2293
|
+
"impersonated"
|
|
2294
|
+
]);
|
|
2295
|
+
const CreateQueriesPermission = z.enum([
|
|
2296
|
+
"query-builder-and-native",
|
|
2297
|
+
"query-builder",
|
|
2298
|
+
"no"
|
|
2299
|
+
]);
|
|
2300
|
+
const DownloadPermission = z.enum(["full", "limited"]);
|
|
2301
|
+
const DataModelPermission = z.literal("all");
|
|
2302
|
+
const YesNoPermission = z.enum(["yes", "no"]);
|
|
2303
|
+
function granular(value) {
|
|
2304
|
+
const perTable = z.record(z.string(), value);
|
|
2305
|
+
const perSchema = z.record(z.string(), z.union([value, perTable]));
|
|
2306
|
+
return z.union([value, perSchema]);
|
|
2307
|
+
}
|
|
2308
|
+
const DatabasePermissions = z.object({
|
|
2309
|
+
"view-data": granular(ViewDataPermission).optional(),
|
|
2310
|
+
"create-queries": granular(CreateQueriesPermission).optional(),
|
|
2311
|
+
download: z.object({ schemas: granular(DownloadPermission) }).loose().optional(),
|
|
2312
|
+
"data-model": z.object({ schemas: granular(DataModelPermission) }).loose().optional(),
|
|
2313
|
+
details: YesNoPermission.optional(),
|
|
2314
|
+
transforms: YesNoPermission.optional()
|
|
2315
|
+
}).loose();
|
|
2316
|
+
const PermissionsGraph = z.object({
|
|
2317
|
+
revision: z.number().int().nonnegative(),
|
|
2318
|
+
groups: z.record(z.string(), z.record(z.string(), DatabasePermissions))
|
|
2319
|
+
}).loose();
|
|
2320
|
+
//#endregion
|
|
2321
|
+
//#region ../client/src/resources/permission.ts
|
|
2322
|
+
function permissionResource(transport) {
|
|
2323
|
+
/**
|
|
2324
|
+
* Get the data permissions of every group on every database, keyed by group id then database
|
|
2325
|
+
* id. Admin only. One read of the whole `data_permissions` table, so prefer `databaseGraph` when
|
|
2326
|
+
* one database is the question.
|
|
2327
|
+
*/
|
|
2328
|
+
async function graph(options = {}) {
|
|
2329
|
+
await transport.require("permission.graph", options);
|
|
2330
|
+
return transport.requestParsed(PermissionsGraph, "/api/permissions/graph", { ...options });
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Get the data permissions of every group on one database. Admin only. The read is filtered to
|
|
2334
|
+
* that database, so this is the cheapest way to learn which groups can query a table: look the
|
|
2335
|
+
* group up under the database and read `view-data` and `create-queries` down to the table id.
|
|
2336
|
+
*/
|
|
2337
|
+
async function databaseGraph(id, options = {}) {
|
|
2338
|
+
await transport.require("permission.databaseGraph", options);
|
|
2339
|
+
return transport.requestParsed(PermissionsGraph, `/api/permissions/graph/db/${id}`, { ...options });
|
|
2340
|
+
}
|
|
2341
|
+
/** Get the data permissions of one group on every database. Admin only. */
|
|
2342
|
+
async function groupGraph(id, options = {}) {
|
|
2343
|
+
await transport.require("permission.groupGraph", options);
|
|
2344
|
+
return transport.requestParsed(PermissionsGraph, `/api/permissions/graph/group/${id}`, { ...options });
|
|
2345
|
+
}
|
|
2346
|
+
return {
|
|
2347
|
+
graph,
|
|
2348
|
+
databaseGraph,
|
|
2349
|
+
groupGraph
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
//#endregion
|
|
2353
|
+
//#region ../client/src/resources/pulse.ts
|
|
2354
|
+
const PulseApiList = z.array(Pulse);
|
|
2355
|
+
function mergePulseUpdate(current, patch) {
|
|
2356
|
+
return {
|
|
2357
|
+
...patch,
|
|
2358
|
+
archived: patch.archived ?? current.archived,
|
|
2359
|
+
skip_if_empty: patch.skip_if_empty ?? current.skip_if_empty
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
function pulseResource(transport) {
|
|
2363
|
+
/** List dashboard subscriptions, optionally narrowed to one dashboard or to the archived ones. */
|
|
2364
|
+
async function list(params = {}, options = {}) {
|
|
2365
|
+
await transport.require("pulse.list", options);
|
|
2366
|
+
return {
|
|
2367
|
+
data: await transport.requestParsed(PulseApiList, "/api/pulse", {
|
|
2368
|
+
...options,
|
|
2369
|
+
query: {
|
|
2370
|
+
dashboard_id: params.dashboard_id,
|
|
2371
|
+
archived: params.archived
|
|
2372
|
+
}
|
|
2373
|
+
}),
|
|
2374
|
+
total: null
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
/** Get one dashboard subscription by id. */
|
|
2378
|
+
async function get(id, options = {}) {
|
|
2379
|
+
await transport.require("pulse.get", options);
|
|
2380
|
+
return transport.requestParsed(Pulse, `/api/pulse/${id}`, { ...options });
|
|
2381
|
+
}
|
|
2382
|
+
/** Create a dashboard subscription — its cards and its delivery channels — from a full body. */
|
|
2383
|
+
async function create(params, options = {}) {
|
|
2384
|
+
await transport.require("pulse.create", options);
|
|
2385
|
+
return transport.requestParsed(Pulse, "/api/pulse", {
|
|
2386
|
+
...options,
|
|
2387
|
+
method: "POST",
|
|
2388
|
+
body: params
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
/** Update a dashboard subscription by id, carrying the server-defaulted flags forward. */
|
|
2392
|
+
async function update(id, params, options = {}) {
|
|
2393
|
+
await transport.require("pulse.update", options);
|
|
2394
|
+
const current = await get(id, options);
|
|
2395
|
+
return transport.requestParsed(Pulse, `/api/pulse/${id}`, {
|
|
2396
|
+
...options,
|
|
2397
|
+
method: "PUT",
|
|
2398
|
+
body: mergePulseUpdate(current, params)
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Archive a dashboard subscription by id, stopping every delivery. Metabase models this as an
|
|
2403
|
+
* update, and disables each of the subscription's channels as a side effect.
|
|
2404
|
+
*/
|
|
2405
|
+
async function archive(id, options = {}) {
|
|
2406
|
+
await transport.require("pulse.archive", options);
|
|
2407
|
+
return update(id, { archived: true }, options);
|
|
2408
|
+
}
|
|
2409
|
+
return {
|
|
2410
|
+
list,
|
|
2411
|
+
get,
|
|
2412
|
+
create,
|
|
2413
|
+
update,
|
|
2414
|
+
archive
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
//#endregion
|
|
2418
|
+
//#region ../client/src/domain/replacement.ts
|
|
2419
|
+
const ReplacementSourceType = z.enum(["card", "table"]);
|
|
2420
|
+
const ReplacementEntityType = z.enum([...ReplacementSourceType.options, "transform"]);
|
|
2421
|
+
const ReplacementRunStatus = z.enum([
|
|
2422
|
+
"pending",
|
|
2423
|
+
"started",
|
|
2424
|
+
"succeeded",
|
|
2425
|
+
"failed",
|
|
2426
|
+
"canceled",
|
|
2427
|
+
"timeout"
|
|
2428
|
+
]);
|
|
2429
|
+
const ReplacementRun = z.object({
|
|
2430
|
+
id: z.number().int(),
|
|
2431
|
+
status: ReplacementRunStatus,
|
|
2432
|
+
is_active: z.literal(true).nullable(),
|
|
2433
|
+
source_entity_type: ReplacementEntityType,
|
|
2434
|
+
source_entity_id: z.number().int(),
|
|
2435
|
+
target_entity_type: ReplacementEntityType,
|
|
2436
|
+
target_entity_id: z.number().int(),
|
|
2437
|
+
progress: z.number().min(0).max(1).nullable(),
|
|
2438
|
+
message: z.string().nullable(),
|
|
2439
|
+
user_id: z.number().int().nullable(),
|
|
2440
|
+
start_time: z.string(),
|
|
2441
|
+
end_time: z.string().nullable()
|
|
2442
|
+
}).loose();
|
|
2443
|
+
ReplacementRun.pick({
|
|
2444
|
+
id: true,
|
|
2445
|
+
status: true,
|
|
2446
|
+
source_entity_type: true,
|
|
2447
|
+
source_entity_id: true,
|
|
2448
|
+
target_entity_type: true,
|
|
2449
|
+
target_entity_id: true,
|
|
2450
|
+
progress: true,
|
|
2451
|
+
message: true,
|
|
2452
|
+
start_time: true,
|
|
2453
|
+
end_time: true
|
|
2454
|
+
}).strip();
|
|
2455
|
+
const ReplacementColumn = z.object({
|
|
2456
|
+
id: z.number().int().nullable(),
|
|
2457
|
+
name: z.string(),
|
|
2458
|
+
display_name: z.string(),
|
|
2459
|
+
base_type: FieldBaseType.nullable(),
|
|
2460
|
+
effective_type: FieldBaseType.nullable(),
|
|
2461
|
+
semantic_type: FieldSemanticType.nullable()
|
|
2462
|
+
});
|
|
2463
|
+
const ReplacementColumnError = z.enum([
|
|
2464
|
+
"column-type-mismatch",
|
|
2465
|
+
"missing-primary-key",
|
|
2466
|
+
"missing-foreign-key",
|
|
2467
|
+
"foreign-key-mismatch"
|
|
2468
|
+
]);
|
|
2469
|
+
const ReplacementColumnMatched = z.object({
|
|
2470
|
+
source: ReplacementColumn,
|
|
2471
|
+
target: ReplacementColumn,
|
|
2472
|
+
errors: z.array(ReplacementColumnError).optional()
|
|
2473
|
+
}).strict();
|
|
2474
|
+
const ReplacementColumnMissingOnTarget = z.object({ source: ReplacementColumn }).strict();
|
|
2475
|
+
const ReplacementColumnMissingOnSource = z.object({ target: ReplacementColumn }).strict();
|
|
2476
|
+
const ReplacementColumnMapping = z.union([
|
|
2477
|
+
ReplacementColumnMatched,
|
|
2478
|
+
ReplacementColumnMissingOnTarget,
|
|
2479
|
+
ReplacementColumnMissingOnSource
|
|
2480
|
+
]);
|
|
2481
|
+
const ReplacementError = z.enum([
|
|
2482
|
+
"cycle-detected",
|
|
2483
|
+
"database-mismatch",
|
|
2484
|
+
"incompatible-implicit-joins",
|
|
2485
|
+
"affects-gtap-policies"
|
|
2486
|
+
]);
|
|
2487
|
+
const ReplacementCheck = z.object({
|
|
2488
|
+
success: z.boolean(),
|
|
2489
|
+
errors: z.array(ReplacementError).optional(),
|
|
2490
|
+
column_mappings: z.array(ReplacementColumnMapping).optional()
|
|
2491
|
+
});
|
|
2492
|
+
z.object({
|
|
2493
|
+
source_entity_id: z.number().int().positive(),
|
|
2494
|
+
source_entity_type: ReplacementSourceType,
|
|
2495
|
+
target_entity_id: z.number().int().positive(),
|
|
2496
|
+
target_entity_type: ReplacementSourceType
|
|
2497
|
+
}).strict();
|
|
2498
|
+
z.object({
|
|
2499
|
+
card_id: z.number().int().positive(),
|
|
2500
|
+
transform_name: z.string(),
|
|
2501
|
+
transform_target: TransformTarget,
|
|
2502
|
+
target_collection_id: z.number().int().positive().nullable().optional(),
|
|
2503
|
+
transform_tag_ids: z.array(z.number().int().positive()).nullable().optional()
|
|
2504
|
+
}).strict();
|
|
2505
|
+
const ReplacementRunStarted = z.object({ run_id: z.number().int().positive() });
|
|
2506
|
+
//#endregion
|
|
2507
|
+
//#region ../client/src/resources/replacement.ts
|
|
2508
|
+
const ReplacementApiRunList = z.array(ReplacementRun);
|
|
2509
|
+
const CancelRunResponse = z.object({ success: z.literal(true) });
|
|
2510
|
+
function replacementResource(transport) {
|
|
2511
|
+
/**
|
|
2512
|
+
* Check whether every usage of a source entity could be rewritten to read a target entity
|
|
2513
|
+
* instead: the two must share a database, the target must not depend on the source, every source
|
|
2514
|
+
* column must exist on the target with a compatible type, and a table source must not be joined
|
|
2515
|
+
* implicitly or sandboxed. Admins only.
|
|
2516
|
+
*/
|
|
2517
|
+
async function checkReplaceSource(params, options = {}) {
|
|
2518
|
+
await transport.require("replacement.checkReplaceSource", options);
|
|
2519
|
+
return transport.requestParsed(ReplacementCheck, "/api/ee/replacement/check-replace-source", {
|
|
2520
|
+
...options,
|
|
2521
|
+
method: "POST",
|
|
2522
|
+
body: params
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Rewrite every usage of a source entity to read a target entity, in the background. Runs the
|
|
2527
|
+
* same check as `checkReplaceSource` first and refuses with a 400 when it fails, and with a 409
|
|
2528
|
+
* while another replacement is running. Admins only.
|
|
2529
|
+
*/
|
|
2530
|
+
async function replaceSource(params, options = {}) {
|
|
2531
|
+
await transport.require("replacement.replaceSource", options);
|
|
2532
|
+
return transport.requestParsed(ReplacementRunStarted, "/api/ee/replacement/replace-source", {
|
|
2533
|
+
...options,
|
|
2534
|
+
method: "POST",
|
|
2535
|
+
body: params
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
/**
|
|
2539
|
+
* Create a transform from a model's query, run it, and rewrite every usage of the model to read
|
|
2540
|
+
* the transform's output table, in the background; the model becomes a saved question. A failed
|
|
2541
|
+
* transform run leaves the model as it was; a failed rewrite keeps the transform and its table
|
|
2542
|
+
* as well, since other queries may already read them. Refuses with a 409 while another
|
|
2543
|
+
* replacement is running. Admins only.
|
|
2544
|
+
*/
|
|
2545
|
+
async function replaceModelWithTransform(params, options = {}) {
|
|
2546
|
+
await transport.require("replacement.replaceModelWithTransform", options);
|
|
2547
|
+
return transport.requestParsed(ReplacementRunStarted, "/api/ee/replacement/replace-model-with-transform", {
|
|
2548
|
+
...options,
|
|
2549
|
+
method: "POST",
|
|
2550
|
+
body: params
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* List replacement runs, newest first. `is-active: true` keeps only the run in progress,
|
|
2555
|
+
* `false` only the ended ones. Admins only.
|
|
2556
|
+
*/
|
|
2557
|
+
async function listRuns(params = {}, options = {}) {
|
|
2558
|
+
await transport.require("replacement.listRuns", options);
|
|
2559
|
+
return {
|
|
2560
|
+
data: await transport.requestParsed(ReplacementApiRunList, "/api/ee/replacement/runs", {
|
|
2561
|
+
...options,
|
|
2562
|
+
query: { "is-active": params["is-active"] }
|
|
2563
|
+
}),
|
|
2564
|
+
total: null
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
/** Get one replacement run by id, with its status and progress. Admins only. */
|
|
2568
|
+
async function getRun(id, options = {}) {
|
|
2569
|
+
await transport.require("replacement.getRun", options);
|
|
2570
|
+
return transport.requestParsed(ReplacementRun, `/api/ee/replacement/runs/${id}`, { ...options });
|
|
2571
|
+
}
|
|
2572
|
+
/** Cancel the replacement in progress. Refuses with a 409 once the run has ended. Admins only. */
|
|
2573
|
+
async function cancelRun(id, options = {}) {
|
|
2574
|
+
await transport.require("replacement.cancelRun", options);
|
|
2575
|
+
await transport.requestParsed(CancelRunResponse, `/api/ee/replacement/runs/${id}/cancel`, {
|
|
2576
|
+
...options,
|
|
2577
|
+
method: "POST"
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
return {
|
|
2581
|
+
checkReplaceSource,
|
|
2582
|
+
replaceSource,
|
|
2583
|
+
replaceModelWithTransform,
|
|
2584
|
+
listRuns,
|
|
2585
|
+
getRun,
|
|
2586
|
+
cancelRun
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
//#endregion
|
|
2590
|
+
//#region ../client/src/domain/revision.ts
|
|
2591
|
+
const RevisionEntity = z.enum([
|
|
2592
|
+
"card",
|
|
2593
|
+
"dashboard",
|
|
2594
|
+
"document",
|
|
2595
|
+
"measure",
|
|
2596
|
+
"segment",
|
|
2597
|
+
"transform"
|
|
2598
|
+
]);
|
|
2599
|
+
const RevisionUser = z.object({
|
|
2600
|
+
id: z.number().int(),
|
|
2601
|
+
first_name: z.string().nullable(),
|
|
2602
|
+
last_name: z.string().nullable(),
|
|
2603
|
+
common_name: z.string()
|
|
2604
|
+
}).loose();
|
|
2605
|
+
const Revision = z.object({
|
|
2606
|
+
id: z.number().int(),
|
|
2607
|
+
timestamp: z.string(),
|
|
2608
|
+
is_creation: z.boolean(),
|
|
2609
|
+
is_reversion: z.boolean(),
|
|
2610
|
+
most_recent: z.boolean(),
|
|
2611
|
+
diff: z.object({}).loose().nullable(),
|
|
2612
|
+
description: z.string(),
|
|
2613
|
+
has_multiple_changes: z.boolean(),
|
|
2614
|
+
user: RevisionUser
|
|
2615
|
+
}).loose();
|
|
2616
|
+
Revision.pick({
|
|
2617
|
+
id: true,
|
|
2618
|
+
timestamp: true,
|
|
2619
|
+
is_creation: true,
|
|
2620
|
+
is_reversion: true,
|
|
2621
|
+
description: true,
|
|
2622
|
+
user: true
|
|
2623
|
+
}).strip();
|
|
2624
|
+
const RevisionRow = z.object({
|
|
2625
|
+
id: z.number().int(),
|
|
2626
|
+
model: z.string(),
|
|
2627
|
+
model_id: z.number().int(),
|
|
2628
|
+
user_id: z.number().int(),
|
|
2629
|
+
object: z.object({}).loose(),
|
|
2630
|
+
timestamp: z.string(),
|
|
2631
|
+
is_creation: z.boolean(),
|
|
2632
|
+
is_reversion: z.boolean(),
|
|
2633
|
+
most_recent: z.boolean()
|
|
2634
|
+
}).loose();
|
|
2635
|
+
z.object({
|
|
2636
|
+
entity: RevisionEntity,
|
|
2637
|
+
id: z.number().int().positive(),
|
|
2638
|
+
revision_id: z.number().int().positive()
|
|
2639
|
+
}).strict();
|
|
2640
|
+
const RevisionReverted = z.object({
|
|
2641
|
+
outcome: z.literal("reverted"),
|
|
2642
|
+
revision: Revision
|
|
2643
|
+
});
|
|
2644
|
+
const RevisionUnchanged = z.object({
|
|
2645
|
+
outcome: z.literal("unchanged"),
|
|
2646
|
+
revision: RevisionRow
|
|
2647
|
+
});
|
|
2648
|
+
z.discriminatedUnion("outcome", [RevisionReverted, RevisionUnchanged]);
|
|
2649
|
+
//#endregion
|
|
2650
|
+
//#region ../client/src/resources/revision.ts
|
|
2651
|
+
const RevisionApiList = z.array(Revision);
|
|
2652
|
+
const RevisionApiRevert = z.union([Revision.transform((revision) => ({
|
|
2653
|
+
outcome: "reverted",
|
|
2654
|
+
revision
|
|
2655
|
+
})), RevisionRow.transform((revision) => ({
|
|
2656
|
+
outcome: "unchanged",
|
|
2657
|
+
revision
|
|
2658
|
+
}))]);
|
|
2659
|
+
const ENTITY_FEATURES = {
|
|
2660
|
+
measure: "measures",
|
|
2661
|
+
transform: "transforms"
|
|
2662
|
+
};
|
|
2663
|
+
function entityFeatures(entity) {
|
|
2664
|
+
const feature = ENTITY_FEATURES[entity];
|
|
2665
|
+
return feature === void 0 ? [] : [feature];
|
|
2666
|
+
}
|
|
2667
|
+
function revisionResource(transport) {
|
|
2668
|
+
/** List the revisions of an entity the caller may read, newest first, each with its diff. */
|
|
2669
|
+
async function list(entity, id, options = {}) {
|
|
2670
|
+
await transport.require("revision.list", options);
|
|
2671
|
+
await transport.requireFeatures(entityFeatures(entity), options);
|
|
2672
|
+
return {
|
|
2673
|
+
data: await transport.requestParsed(RevisionApiList, `/api/revision/${entity}/${id}`, { ...options }),
|
|
2674
|
+
total: null
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* Revert an entity to one of its prior revisions. Needs write access to the entity, and for a
|
|
2679
|
+
* card the permission to run the query being restored.
|
|
2680
|
+
*/
|
|
2681
|
+
async function revert(params, options = {}) {
|
|
2682
|
+
await transport.require("revision.revert", options);
|
|
2683
|
+
await transport.requireFeatures(entityFeatures(params.entity), options);
|
|
2684
|
+
return transport.requestParsed(RevisionApiRevert, "/api/revision/revert", {
|
|
2685
|
+
...options,
|
|
2686
|
+
method: "POST",
|
|
2687
|
+
body: params
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
return {
|
|
2691
|
+
list,
|
|
2692
|
+
revert
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
//#endregion
|
|
2696
|
+
//#region ../client/src/resources/search.ts
|
|
2697
|
+
const SearchApiResponse = z.object({
|
|
2698
|
+
data: z.array(SearchResult),
|
|
2699
|
+
total: z.number().int().nonnegative()
|
|
2700
|
+
}).loose();
|
|
2701
|
+
function searchResource(transport) {
|
|
2702
|
+
/**
|
|
2703
|
+
* Search over the instance's content, ranked against `q`. `models` narrows which kinds of entity
|
|
2704
|
+
* may match, `archived` swaps the active set for the archived one, `table_db_id` restricts to
|
|
2705
|
+
* items on one database, `verified` to verified content, `collection` to one collection and its
|
|
2706
|
+
* descendants, `created_by` to items created by any of those users, `search_native_query` also
|
|
2707
|
+
* matches the text of native queries, `include_metadata` attaches each card's `result_metadata`,
|
|
2708
|
+
* and `limit`/`offset` are the window the server applies before ranking hydration.
|
|
2709
|
+
*/
|
|
2710
|
+
async function query(params = {}, options = {}) {
|
|
2711
|
+
await transport.require("search.query", options);
|
|
2712
|
+
const response = await transport.requestParsed(SearchApiResponse, "/api/search", {
|
|
2713
|
+
...options,
|
|
2714
|
+
query: {
|
|
2715
|
+
q: params.q,
|
|
2716
|
+
models: params.models,
|
|
2717
|
+
archived: params.archived,
|
|
2718
|
+
limit: params.limit,
|
|
2719
|
+
offset: params.offset,
|
|
2720
|
+
table_db_id: params.table_db_id,
|
|
2721
|
+
verified: params.verified,
|
|
2722
|
+
collection: params.collection,
|
|
2723
|
+
created_by: params.created_by,
|
|
2724
|
+
search_native_query: params.search_native_query,
|
|
2725
|
+
include_metadata: params.include_metadata
|
|
2726
|
+
}
|
|
2727
|
+
});
|
|
2728
|
+
return {
|
|
2729
|
+
data: response.data,
|
|
2730
|
+
total: response.total
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
return { query };
|
|
2734
|
+
}
|
|
2735
|
+
//#endregion
|
|
2736
|
+
//#region ../client/src/resources/segment.ts
|
|
2737
|
+
const SegmentApiList = z.array(Segment);
|
|
2738
|
+
function segmentResource(transport) {
|
|
2739
|
+
/** List every segment the caller can see. */
|
|
2740
|
+
async function list(options = {}) {
|
|
2741
|
+
await transport.require("segment.list", options);
|
|
2742
|
+
return {
|
|
2743
|
+
data: await transport.requestParsed(SegmentApiList, "/api/segment", { ...options }),
|
|
2744
|
+
total: null
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
/** Get one segment by id. */
|
|
2748
|
+
async function get(id, options = {}) {
|
|
2749
|
+
await transport.require("segment.get", options);
|
|
2750
|
+
return transport.requestParsed(Segment, `/api/segment/${id}`, { ...options });
|
|
2751
|
+
}
|
|
2752
|
+
/** Create a segment — a reusable row filter over one table — from a full segment body. */
|
|
2753
|
+
async function create(params, options = {}) {
|
|
2754
|
+
await transport.require("segment.create", options);
|
|
2755
|
+
return transport.requestParsed(Segment, "/api/segment", {
|
|
2756
|
+
...options,
|
|
2757
|
+
method: "POST",
|
|
2758
|
+
body: params
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Update a segment by id, patching only the fields the body carries. `revision_message` is
|
|
2763
|
+
* required: Metabase records every segment change in its revision history.
|
|
2764
|
+
*/
|
|
2765
|
+
async function update(id, params, options = {}) {
|
|
2766
|
+
await transport.require("segment.update", options);
|
|
2767
|
+
return transport.requestParsed(Segment, `/api/segment/${id}`, {
|
|
2768
|
+
...options,
|
|
2769
|
+
method: "PUT",
|
|
2770
|
+
body: params
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
/**
|
|
2774
|
+
* Archive (soft-delete) a segment by id. Metabase models this as an update, so it carries the
|
|
2775
|
+
* same required `revision_message`.
|
|
2776
|
+
*/
|
|
2777
|
+
async function archive(id, params, options = {}) {
|
|
2778
|
+
await transport.require("segment.archive", options);
|
|
2779
|
+
return update(id, {
|
|
2780
|
+
archived: true,
|
|
2781
|
+
revision_message: params.revision_message
|
|
2782
|
+
}, options);
|
|
2783
|
+
}
|
|
2784
|
+
return {
|
|
2785
|
+
list,
|
|
2786
|
+
get,
|
|
2787
|
+
create,
|
|
2788
|
+
update,
|
|
2789
|
+
archive
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
//#endregion
|
|
2793
|
+
//#region ../client/src/resources/setting.ts
|
|
2794
|
+
const SettingApiList = z.array(Setting);
|
|
2795
|
+
const UNKNOWN_SETTING_PREFIX = "Unknown setting:";
|
|
2796
|
+
function rethrowSettingError(error, key) {
|
|
2797
|
+
if (errorMessage(error).startsWith(UNKNOWN_SETTING_PREFIX)) throw new ConfigError(`unknown setting: ${key}`);
|
|
2798
|
+
throw error;
|
|
2799
|
+
}
|
|
2800
|
+
function settingResource(transport) {
|
|
2801
|
+
/** Get all settings and their values, with descriptions and environment-variable names. */
|
|
2802
|
+
async function list(options = {}) {
|
|
2803
|
+
await transport.require("setting.list", options);
|
|
2804
|
+
return {
|
|
2805
|
+
data: await transport.requestParsed(SettingApiList, "/api/setting", { ...options }),
|
|
2806
|
+
total: null
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Fetch a single setting's value by key. An unset setting answers 204 rather than 404, which
|
|
2811
|
+
* reaches the caller as `null`; a string-typed setting answers bare text rather than JSON.
|
|
2812
|
+
*/
|
|
2813
|
+
async function get(key, options = {}) {
|
|
2814
|
+
await transport.require("setting.get", options);
|
|
2815
|
+
return fetchOptionalParsed(transport, `/api/setting/${encodeURIComponent(key)}`, z.unknown(), options).catch((error) => rethrowSettingError(error, key));
|
|
2816
|
+
}
|
|
2817
|
+
/** Set a single setting's value by key. The endpoint answers 204 with no body. */
|
|
2818
|
+
async function set(key, value, options = {}) {
|
|
2819
|
+
await transport.require("setting.set", options);
|
|
2820
|
+
await transport.requestRaw(`/api/setting/${encodeURIComponent(key)}`, {
|
|
2821
|
+
...options,
|
|
2822
|
+
method: "PUT",
|
|
2823
|
+
body: { value },
|
|
2824
|
+
expectContentType: "binary"
|
|
2825
|
+
}).catch((error) => rethrowSettingError(error, key));
|
|
2826
|
+
}
|
|
2827
|
+
return {
|
|
2828
|
+
list,
|
|
2829
|
+
get,
|
|
2830
|
+
set
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
//#endregion
|
|
2834
|
+
//#region ../client/src/resources/setup.ts
|
|
2835
|
+
function setupResource(transport) {
|
|
2836
|
+
/**
|
|
2837
|
+
* Complete the initial setup wizard on a fresh instance, creating its first admin user and
|
|
2838
|
+
* applying the instance preferences. The setup token is single-use.
|
|
2839
|
+
*/
|
|
2840
|
+
async function create(params, options = {}) {
|
|
2841
|
+
await transport.require("setup.create", options);
|
|
2842
|
+
return transport.requestParsed(SetupResult, "/api/setup", {
|
|
2843
|
+
...options,
|
|
2844
|
+
method: "POST",
|
|
2845
|
+
body: params
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
return { create };
|
|
2849
|
+
}
|
|
2850
|
+
//#endregion
|
|
2851
|
+
//#region ../client/src/resources/snippet.ts
|
|
2852
|
+
const SnippetApiList = z.array(Snippet);
|
|
2853
|
+
function snippetResource(transport) {
|
|
2854
|
+
/** List native query snippets. `archived` swaps the listing to archived snippets. */
|
|
2855
|
+
async function list(params = {}, options = {}) {
|
|
2856
|
+
await transport.require("snippet.list", options);
|
|
2857
|
+
return {
|
|
2858
|
+
data: await transport.requestParsed(SnippetApiList, "/api/native-query-snippet", {
|
|
2859
|
+
...options,
|
|
2860
|
+
query: { archived: params.archived }
|
|
2861
|
+
}),
|
|
2862
|
+
total: null
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
/** Get one native query snippet by id. */
|
|
2866
|
+
async function get(id, options = {}) {
|
|
2867
|
+
await transport.require("snippet.get", options);
|
|
2868
|
+
return transport.requestParsed(Snippet, `/api/native-query-snippet/${id}`, { ...options });
|
|
2869
|
+
}
|
|
2870
|
+
/** Create a native query snippet from a name, its SQL fragment, and an optional collection. */
|
|
2871
|
+
async function create(params, options = {}) {
|
|
2872
|
+
await transport.require("snippet.create", options);
|
|
2873
|
+
return transport.requestParsed(Snippet, "/api/native-query-snippet", {
|
|
2874
|
+
...options,
|
|
2875
|
+
method: "POST",
|
|
2876
|
+
body: params
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
/** Update a native query snippet by id, patching only the fields the body carries. */
|
|
2880
|
+
async function update(id, params, options = {}) {
|
|
2881
|
+
await transport.require("snippet.update", options);
|
|
2882
|
+
return transport.requestParsed(Snippet, `/api/native-query-snippet/${id}`, {
|
|
2883
|
+
...options,
|
|
2884
|
+
method: "PUT",
|
|
2885
|
+
body: params
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* Archive (soft-delete) a native query snippet by id. Metabase models this as an update, not its
|
|
2890
|
+
* own endpoint.
|
|
2891
|
+
*/
|
|
2892
|
+
async function archive(id, options = {}) {
|
|
2893
|
+
await transport.require("snippet.archive", options);
|
|
2894
|
+
return update(id, { archived: true }, options);
|
|
2895
|
+
}
|
|
2896
|
+
return {
|
|
2897
|
+
list,
|
|
2898
|
+
get,
|
|
2899
|
+
create,
|
|
2900
|
+
update,
|
|
2901
|
+
archive
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
//#endregion
|
|
2905
|
+
//#region ../client/src/resources/table.ts
|
|
2906
|
+
const TableApiList = z.array(Table);
|
|
2907
|
+
const TableApiFks = z.array(TableForeignKey);
|
|
2908
|
+
const SyncSchemaResponse = z.object({ status: z.literal("ok") });
|
|
2909
|
+
const RescanValuesResponse = z.object({ status: z.literal("success") });
|
|
2910
|
+
const BulkEditResponse = z.object({});
|
|
2911
|
+
const BULK_EDIT_PATH = "/api/data-studio/table/edit";
|
|
2912
|
+
const LIST_PARAM_FEATURES = [
|
|
2913
|
+
["can-query", "tableListAccessFilters"],
|
|
2914
|
+
["can-write", "tableListAccessFilters"],
|
|
2915
|
+
["include-transform-targets", "tableListTransformTargets"],
|
|
2916
|
+
["unused-only", "tableUnusedFilter"]
|
|
2917
|
+
];
|
|
2918
|
+
function listParamFeatures(params) {
|
|
2919
|
+
return LIST_PARAM_FEATURES.filter(([param]) => params[param] !== void 0).map(([, feature]) => feature);
|
|
2920
|
+
}
|
|
2921
|
+
const UPLOAD_UPDATE_PATHS = {
|
|
2922
|
+
append: "append-csv",
|
|
2923
|
+
replace: "replace-csv"
|
|
2924
|
+
};
|
|
2925
|
+
function tableResource(transport) {
|
|
2926
|
+
/**
|
|
2927
|
+
* List every table the caller can see, across all databases. `term` matches names and display
|
|
2928
|
+
* names; `can-query` and `can-write` keep only the tables the caller may query or edit the
|
|
2929
|
+
* metadata of; `include-transform-targets` adds tables a transform writes to; `unused-only` keeps
|
|
2930
|
+
* tables nothing depends on.
|
|
2931
|
+
*/
|
|
2932
|
+
async function list(params = {}, options = {}) {
|
|
2933
|
+
await transport.require("table.list", options);
|
|
2934
|
+
await transport.requireFeatures(listParamFeatures(params), options);
|
|
2935
|
+
await requireDataLayer(params["data-layer"], options);
|
|
2936
|
+
return {
|
|
2937
|
+
data: await transport.requestParsed(TableApiList, "/api/table", {
|
|
2938
|
+
...options,
|
|
2939
|
+
query: {
|
|
2940
|
+
term: params.term,
|
|
2941
|
+
"visibility-type": params["visibility-type"],
|
|
2942
|
+
"data-layer": params["data-layer"],
|
|
2943
|
+
"data-source": params["data-source"],
|
|
2944
|
+
"owner-user-id": params["owner-user-id"],
|
|
2945
|
+
"owner-email": params["owner-email"],
|
|
2946
|
+
"orphan-only": params["orphan-only"],
|
|
2947
|
+
"unused-only": params["unused-only"],
|
|
2948
|
+
"can-query": params["can-query"],
|
|
2949
|
+
"can-write": params["can-write"],
|
|
2950
|
+
"include-transform-targets": params["include-transform-targets"]
|
|
2951
|
+
}
|
|
2952
|
+
}),
|
|
2953
|
+
total: null
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
/** Get one table by id, without its fields. */
|
|
2957
|
+
async function get(id, options = {}) {
|
|
2958
|
+
await transport.require("table.get", options);
|
|
2959
|
+
return transport.requestParsed(Table, `/api/table/${id}`, { ...options });
|
|
2960
|
+
}
|
|
2961
|
+
/** Update a table by id, patching only the fields the body carries. */
|
|
2962
|
+
async function update(id, params, options = {}) {
|
|
2963
|
+
await transport.require("table.update", options);
|
|
2964
|
+
await requireDataLayer(params.data_layer, options);
|
|
2965
|
+
return transport.requestParsed(Table, `/api/table/${id}`, {
|
|
2966
|
+
...options,
|
|
2967
|
+
method: "PUT",
|
|
2968
|
+
body: params
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
async function requireDataLayer(value, options) {
|
|
2972
|
+
if (value === null || value === void 0) return;
|
|
2973
|
+
if (TableDataLayerTier.safeParse(value).success) {
|
|
2974
|
+
await transport.requireFeatures(["tableDataLayerTiers"], options);
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
const { features } = await transport.server(options);
|
|
2978
|
+
if (features.tableDataLayerTiers) throw new ConfigError(`data_layer "${value}" is a medallion name; this server names a table's layer ${TableDataLayerTier.options.join(", ")}`);
|
|
2979
|
+
}
|
|
2980
|
+
/** Get a table by id with its fields hydrated — the metadata the query builder runs on. */
|
|
2981
|
+
async function queryMetadata(id, options = {}) {
|
|
2982
|
+
await transport.require("table.queryMetadata", options);
|
|
2983
|
+
return transport.requestParsed(TableQueryMetadata, `/api/table/${id}/query_metadata`, { ...options });
|
|
2984
|
+
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Get every foreign key whose destination is a field of this table. A table with no active
|
|
2987
|
+
* fields answers no content, which is the same empty list.
|
|
2988
|
+
*/
|
|
2989
|
+
async function fks(id, options = {}) {
|
|
2990
|
+
await transport.require("table.fks", options);
|
|
2991
|
+
return {
|
|
2992
|
+
data: await fetchOptionalParsed(transport, `/api/table/${id}/fks`, TableApiFks, { ...options }) ?? [],
|
|
2993
|
+
total: null
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Trigger a manual update of this table's schema metadata. The sync runs after the call returns;
|
|
2998
|
+
* a warehouse the server cannot connect to is a 422.
|
|
2999
|
+
*/
|
|
3000
|
+
async function syncSchema(id, options = {}) {
|
|
3001
|
+
await transport.require("table.syncSchema", options);
|
|
3002
|
+
await transport.requestParsed(SyncSchemaResponse, `/api/table/${id}/sync_schema`, {
|
|
3003
|
+
...options,
|
|
3004
|
+
method: "POST"
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Trigger an update of the field values of every eligible field of this table. The scan runs
|
|
3009
|
+
* after the call returns.
|
|
3010
|
+
*/
|
|
3011
|
+
async function rescanValues(id, options = {}) {
|
|
3012
|
+
await transport.require("table.rescanValues", options);
|
|
3013
|
+
await transport.requestParsed(RescanValuesResponse, `/api/table/${id}/rescan_values`, {
|
|
3014
|
+
...options,
|
|
3015
|
+
method: "POST"
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
/**
|
|
3019
|
+
* Set the same metadata on every table the selectors pick out. A table leaving the `hidden` data
|
|
3020
|
+
* layer is re-synced after the call returns.
|
|
3021
|
+
*/
|
|
3022
|
+
async function bulkEdit(params, options = {}) {
|
|
3023
|
+
await transport.require("table.bulkEdit", options);
|
|
3024
|
+
await transport.requestParsed(BulkEditResponse, BULK_EDIT_PATH, {
|
|
3025
|
+
...options,
|
|
3026
|
+
method: "POST",
|
|
3027
|
+
body: params
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
async function updateFromCsv(id, action, file, options) {
|
|
3031
|
+
await transport.requestRaw(`/api/table/${id}/${UPLOAD_UPDATE_PATHS[action]}`, {
|
|
3032
|
+
...options,
|
|
3033
|
+
method: "POST",
|
|
3034
|
+
body: buildCsvFormData(file),
|
|
3035
|
+
expectContentType: "binary"
|
|
3036
|
+
});
|
|
3037
|
+
return {
|
|
3038
|
+
table_id: id,
|
|
3039
|
+
action
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
/**
|
|
3043
|
+
* Inserts the rows of an uploaded CSV file into the table identified by `id`. The table must have
|
|
3044
|
+
* been created by uploading a CSV file.
|
|
3045
|
+
*
|
|
3046
|
+
* The file may be at most 50 MB; larger uploads are rejected with a 413 response.
|
|
3047
|
+
*/
|
|
3048
|
+
async function appendCsv(id, file, options = {}) {
|
|
3049
|
+
await transport.require("table.appendCsv", options);
|
|
3050
|
+
return updateFromCsv(id, "append", file, options);
|
|
3051
|
+
}
|
|
3052
|
+
/**
|
|
3053
|
+
* Replaces the contents of the table identified by `id` with the rows of an uploaded CSV file.
|
|
3054
|
+
* The table must have been created by uploading a CSV file.
|
|
3055
|
+
*
|
|
3056
|
+
* The file may be at most 50 MB; larger uploads are rejected with a 413 response.
|
|
3057
|
+
*/
|
|
3058
|
+
async function replaceCsv(id, file, options = {}) {
|
|
3059
|
+
await transport.require("table.replaceCsv", options);
|
|
3060
|
+
return updateFromCsv(id, "replace", file, options);
|
|
3061
|
+
}
|
|
3062
|
+
return {
|
|
3063
|
+
list,
|
|
3064
|
+
get,
|
|
3065
|
+
update,
|
|
3066
|
+
queryMetadata,
|
|
3067
|
+
fks,
|
|
3068
|
+
syncSchema,
|
|
3069
|
+
rescanValues,
|
|
3070
|
+
bulkEdit,
|
|
3071
|
+
appendCsv,
|
|
3072
|
+
replaceCsv
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
//#endregion
|
|
3076
|
+
//#region ../client/src/resources/timeline-event.ts
|
|
3077
|
+
function timelineEventResource(transport) {
|
|
3078
|
+
/** Get one timeline event by id. */
|
|
3079
|
+
async function get(id, options = {}) {
|
|
3080
|
+
await transport.require("timelineEvent.get", options);
|
|
3081
|
+
return transport.requestParsed(TimelineEvent, `/api/timeline-event/${id}`, { ...options });
|
|
3082
|
+
}
|
|
3083
|
+
/** Create a timeline event on an existing timeline. */
|
|
3084
|
+
async function create(params, options = {}) {
|
|
3085
|
+
await transport.require("timelineEvent.create", options);
|
|
3086
|
+
return transport.requestParsed(TimelineEvent, "/api/timeline-event", {
|
|
3087
|
+
...options,
|
|
3088
|
+
method: "POST",
|
|
3089
|
+
body: params
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
/**
|
|
3093
|
+
* Update a timeline event by id, patching only the fields the body carries. `timeline_id` moves
|
|
3094
|
+
* the event to another timeline.
|
|
3095
|
+
*/
|
|
3096
|
+
async function update(id, params, options = {}) {
|
|
3097
|
+
await transport.require("timelineEvent.update", options);
|
|
3098
|
+
return transport.requestParsed(TimelineEvent, `/api/timeline-event/${id}`, {
|
|
3099
|
+
...options,
|
|
3100
|
+
method: "PUT",
|
|
3101
|
+
body: params
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
/**
|
|
3105
|
+
* Archive (soft-delete) a timeline event by id. Metabase models this as an update, not its own
|
|
3106
|
+
* endpoint.
|
|
3107
|
+
*/
|
|
3108
|
+
async function archive(id, options = {}) {
|
|
3109
|
+
await transport.require("timelineEvent.archive", options);
|
|
3110
|
+
return update(id, { archived: true }, options);
|
|
3111
|
+
}
|
|
3112
|
+
/** Permanently delete a timeline event by id. The server answers with no body. */
|
|
3113
|
+
async function remove(id, options = {}) {
|
|
3114
|
+
await transport.require("timelineEvent.delete", options);
|
|
3115
|
+
await transport.requestRaw(`/api/timeline-event/${id}`, {
|
|
3116
|
+
...options,
|
|
3117
|
+
method: "DELETE",
|
|
3118
|
+
expectContentType: "binary"
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
return {
|
|
3122
|
+
get,
|
|
3123
|
+
create,
|
|
3124
|
+
update,
|
|
3125
|
+
archive,
|
|
3126
|
+
delete: remove
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
//#endregion
|
|
3130
|
+
//#region ../client/src/resources/timeline.ts
|
|
3131
|
+
const TimelineApiList = z.array(Timeline);
|
|
3132
|
+
const TimelineWithEvents = Timeline.required({ events: true });
|
|
3133
|
+
function timelineResource(transport) {
|
|
3134
|
+
/** List timelines. `archived` swaps the active listing for the archived one. */
|
|
3135
|
+
async function list(params = {}, options = {}) {
|
|
3136
|
+
await transport.require("timeline.list", options);
|
|
3137
|
+
return {
|
|
3138
|
+
data: await transport.requestParsed(TimelineApiList, "/api/timeline", {
|
|
3139
|
+
...options,
|
|
3140
|
+
query: { archived: params.archived }
|
|
3141
|
+
}),
|
|
3142
|
+
total: null
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
/** Get one timeline by id, without its events. */
|
|
3146
|
+
async function get(id, options = {}) {
|
|
3147
|
+
await transport.require("timeline.get", options);
|
|
3148
|
+
return transport.requestParsed(Timeline, `/api/timeline/${id}`, { ...options });
|
|
3149
|
+
}
|
|
3150
|
+
/**
|
|
3151
|
+
* List the events on one timeline. Metabase has no endpoint of its own for these: the same
|
|
3152
|
+
* timeline read hydrates them on request, and `archived` chooses which of them come back.
|
|
3153
|
+
*/
|
|
3154
|
+
async function events(id, params = {}, options = {}) {
|
|
3155
|
+
await transport.require("timeline.events", options);
|
|
3156
|
+
return {
|
|
3157
|
+
data: (await transport.requestParsed(TimelineWithEvents, `/api/timeline/${id}`, {
|
|
3158
|
+
...options,
|
|
3159
|
+
query: {
|
|
3160
|
+
include: "events",
|
|
3161
|
+
archived: params.archived
|
|
3162
|
+
}
|
|
3163
|
+
})).events,
|
|
3164
|
+
total: null
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
/** Create a timeline from a full timeline body. */
|
|
3168
|
+
async function create(params, options = {}) {
|
|
3169
|
+
await transport.require("timeline.create", options);
|
|
3170
|
+
return transport.requestParsed(Timeline, "/api/timeline", {
|
|
3171
|
+
...options,
|
|
3172
|
+
method: "POST",
|
|
3173
|
+
body: params
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
/**
|
|
3177
|
+
* Update a timeline by id, patching only the fields the body carries. Changing `archived`
|
|
3178
|
+
* cascades to every event on the timeline.
|
|
3179
|
+
*/
|
|
3180
|
+
async function update(id, params, options = {}) {
|
|
3181
|
+
await transport.require("timeline.update", options);
|
|
3182
|
+
return transport.requestParsed(Timeline, `/api/timeline/${id}`, {
|
|
3183
|
+
...options,
|
|
3184
|
+
method: "PUT",
|
|
3185
|
+
body: params
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Archive (soft-delete) a timeline and all its events by id. Metabase models this as an update,
|
|
3190
|
+
* not its own endpoint.
|
|
3191
|
+
*/
|
|
3192
|
+
async function archive(id, options = {}) {
|
|
3193
|
+
await transport.require("timeline.archive", options);
|
|
3194
|
+
return update(id, { archived: true }, options);
|
|
3195
|
+
}
|
|
3196
|
+
/** Permanently delete a timeline and all its events by id. The server answers with no body. */
|
|
3197
|
+
async function remove(id, options = {}) {
|
|
3198
|
+
await transport.require("timeline.delete", options);
|
|
3199
|
+
await transport.requestRaw(`/api/timeline/${id}`, {
|
|
3200
|
+
...options,
|
|
3201
|
+
method: "DELETE",
|
|
3202
|
+
expectContentType: "binary"
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
return {
|
|
3206
|
+
list,
|
|
3207
|
+
get,
|
|
3208
|
+
events,
|
|
3209
|
+
create,
|
|
3210
|
+
update,
|
|
3211
|
+
archive,
|
|
3212
|
+
delete: remove
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3215
|
+
//#endregion
|
|
3216
|
+
//#region ../client/src/domain/transform-dag-run.ts
|
|
3217
|
+
const TransformMemberRun = TransformRun.extend({
|
|
3218
|
+
job_run_id: z.number().int().nullable(),
|
|
3219
|
+
dag_run_id: z.number().int().nullable()
|
|
3220
|
+
});
|
|
3221
|
+
TransformMemberRun.pick({
|
|
3222
|
+
id: true,
|
|
3223
|
+
transform_id: true,
|
|
3224
|
+
dag_run_id: true,
|
|
3225
|
+
status: true,
|
|
3226
|
+
run_method: true,
|
|
3227
|
+
start_time: true,
|
|
3228
|
+
end_time: true,
|
|
3229
|
+
message: true
|
|
3230
|
+
}).strip();
|
|
3231
|
+
//#endregion
|
|
3232
|
+
//#region ../client/src/resources/transform-dag-run.ts
|
|
3233
|
+
const TransformMemberRunList = z.array(TransformMemberRun);
|
|
3234
|
+
function transformDagRunResource(transport) {
|
|
3235
|
+
/**
|
|
3236
|
+
* List the transform runs a DAG run coordinated so far. A member run is inserted when its
|
|
3237
|
+
* transform starts, so the list grows while the DAG run is in progress.
|
|
3238
|
+
*/
|
|
3239
|
+
async function transformRuns(runId, options = {}) {
|
|
3240
|
+
await transport.require("transformDagRun.transformRuns", options);
|
|
3241
|
+
return {
|
|
3242
|
+
data: await transport.requestParsed(TransformMemberRunList, `/api/transform-dag-run/${runId}/transform-runs`, { ...options }),
|
|
3243
|
+
total: null
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
/** Cancel an in-progress DAG run and request cancellation of its still-running transforms. */
|
|
3247
|
+
async function cancel(runId, options = {}) {
|
|
3248
|
+
await transport.require("transformDagRun.cancel", options);
|
|
3249
|
+
await transport.requestRaw(`/api/transform-dag-run/${runId}/cancel`, {
|
|
3250
|
+
...options,
|
|
3251
|
+
method: "POST",
|
|
3252
|
+
expectContentType: "binary"
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
return {
|
|
3256
|
+
transformRuns,
|
|
3257
|
+
cancel
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
//#endregion
|
|
3261
|
+
//#region ../client/src/domain/transform-inspector.ts
|
|
3262
|
+
const InspectorFieldStats = z.object({
|
|
3263
|
+
distinct_count: z.number().int().optional(),
|
|
3264
|
+
nil_percent: z.number().optional(),
|
|
3265
|
+
min: z.number().optional(),
|
|
3266
|
+
max: z.number().optional(),
|
|
3267
|
+
avg: z.number().optional(),
|
|
3268
|
+
q1: z.number().optional(),
|
|
3269
|
+
q3: z.number().optional(),
|
|
3270
|
+
earliest: z.string().optional(),
|
|
3271
|
+
latest: z.string().optional()
|
|
3272
|
+
}).loose();
|
|
3273
|
+
const InspectorField = z.object({
|
|
3274
|
+
id: z.number().int(),
|
|
3275
|
+
name: z.string(),
|
|
3276
|
+
display_name: z.string().nullable().optional(),
|
|
3277
|
+
base_type: z.string(),
|
|
3278
|
+
semantic_type: z.string().nullable().optional(),
|
|
3279
|
+
stats: InspectorFieldStats.nullable().optional()
|
|
3280
|
+
}).loose();
|
|
3281
|
+
const InspectorTable = z.object({
|
|
3282
|
+
table_id: z.number().int(),
|
|
3283
|
+
table_name: z.string(),
|
|
3284
|
+
schema: z.string().nullable().optional(),
|
|
3285
|
+
db_id: z.number().int(),
|
|
3286
|
+
column_count: z.number().int(),
|
|
3287
|
+
fields: z.array(InspectorField)
|
|
3288
|
+
}).loose();
|
|
3289
|
+
const LensComplexityLevel = z.enum([
|
|
3290
|
+
"fast",
|
|
3291
|
+
"slow",
|
|
3292
|
+
"very-slow"
|
|
3293
|
+
]);
|
|
3294
|
+
const LensComplexity = z.object({
|
|
3295
|
+
level: LensComplexityLevel,
|
|
3296
|
+
score: z.number().int().optional()
|
|
3297
|
+
}).loose();
|
|
3298
|
+
const LensMetadata = z.object({
|
|
3299
|
+
id: z.string(),
|
|
3300
|
+
display_name: z.string(),
|
|
3301
|
+
description: z.string().nullable().optional(),
|
|
3302
|
+
complexity: LensComplexity.optional()
|
|
3303
|
+
}).loose();
|
|
3304
|
+
const TransformInspectionStatus = z.enum(["not-run", "ready"]);
|
|
3305
|
+
const TransformInspection = z.object({
|
|
3306
|
+
name: z.string(),
|
|
3307
|
+
description: z.string().nullable().optional(),
|
|
3308
|
+
status: TransformInspectionStatus,
|
|
3309
|
+
sources: z.array(InspectorTable),
|
|
3310
|
+
target: InspectorTable.nullable().optional(),
|
|
3311
|
+
visited_fields: z.object({ all: z.array(z.number().int()).optional() }).loose().nullable().optional(),
|
|
3312
|
+
available_lenses: z.array(LensMetadata)
|
|
3313
|
+
}).loose();
|
|
3314
|
+
const LensCardDisplay = z.enum([
|
|
3315
|
+
"bar",
|
|
3316
|
+
"row",
|
|
3317
|
+
"line",
|
|
3318
|
+
"area",
|
|
3319
|
+
"pie",
|
|
3320
|
+
"scalar",
|
|
3321
|
+
"gauge",
|
|
3322
|
+
"progress",
|
|
3323
|
+
"table",
|
|
3324
|
+
"hidden"
|
|
3325
|
+
]);
|
|
3326
|
+
const LensJoinMetadata = z.object({
|
|
3327
|
+
join_step: z.number().int().optional(),
|
|
3328
|
+
join_alias: z.string().optional(),
|
|
3329
|
+
join_strategy: z.string().optional()
|
|
3330
|
+
}).loose();
|
|
3331
|
+
const LensCardMetadata = LensJoinMetadata.extend({
|
|
3332
|
+
dedup_key: z.array(z.unknown()).optional(),
|
|
3333
|
+
depends_on_cards: z.array(z.string()).optional(),
|
|
3334
|
+
group_id: z.string().optional(),
|
|
3335
|
+
group_role: z.enum(["input", "output"]).optional(),
|
|
3336
|
+
group_order: z.number().int().optional(),
|
|
3337
|
+
card_type: z.string().optional()
|
|
3338
|
+
});
|
|
3339
|
+
const LensCard = z.object({
|
|
3340
|
+
id: z.string(),
|
|
3341
|
+
section_id: z.string().nullable().optional(),
|
|
3342
|
+
title: z.string(),
|
|
3343
|
+
display: LensCardDisplay,
|
|
3344
|
+
dataset_query: DatasetQuery,
|
|
3345
|
+
metadata: LensCardMetadata.optional()
|
|
3346
|
+
}).loose();
|
|
3347
|
+
const LensSection = z.object({
|
|
3348
|
+
id: z.string(),
|
|
3349
|
+
title: z.string(),
|
|
3350
|
+
description: z.string().nullable().optional(),
|
|
3351
|
+
layout: z.enum(["flat", "comparison"]).optional()
|
|
3352
|
+
}).loose();
|
|
3353
|
+
const LensHighlight = z.object({
|
|
3354
|
+
label: z.string(),
|
|
3355
|
+
value: z.unknown().optional(),
|
|
3356
|
+
card_id: z.string().optional()
|
|
3357
|
+
}).loose();
|
|
3358
|
+
const LensSummary = z.object({
|
|
3359
|
+
text: z.string().nullable().optional(),
|
|
3360
|
+
highlights: z.array(LensHighlight).optional(),
|
|
3361
|
+
alerts: z.array(z.object({}).loose()).optional()
|
|
3362
|
+
}).loose();
|
|
3363
|
+
const LensTriggerCondition = z.object({ name: z.string() }).loose();
|
|
3364
|
+
const LensAlertTrigger = z.object({
|
|
3365
|
+
id: z.string(),
|
|
3366
|
+
condition: LensTriggerCondition,
|
|
3367
|
+
severity: z.enum([
|
|
3368
|
+
"info",
|
|
3369
|
+
"warning",
|
|
3370
|
+
"error"
|
|
3371
|
+
]),
|
|
3372
|
+
message: z.string(),
|
|
3373
|
+
metadata: LensJoinMetadata.optional()
|
|
3374
|
+
}).loose();
|
|
3375
|
+
/** The parameters a drill lens takes; `join_step` scopes a join lens to one join. */
|
|
3376
|
+
const LensParams = z.object({ join_step: z.number().int().optional() });
|
|
3377
|
+
const LensDrillTrigger = z.object({
|
|
3378
|
+
lens_id: z.string(),
|
|
3379
|
+
condition: LensTriggerCondition,
|
|
3380
|
+
params: LensParams.loose().optional(),
|
|
3381
|
+
reason: z.string().optional(),
|
|
3382
|
+
metadata: LensJoinMetadata.optional()
|
|
3383
|
+
}).loose();
|
|
3384
|
+
const TransformLens = z.object({
|
|
3385
|
+
id: z.string(),
|
|
3386
|
+
display_name: z.string(),
|
|
3387
|
+
complexity: LensComplexity.optional(),
|
|
3388
|
+
summary: LensSummary.optional(),
|
|
3389
|
+
sections: z.array(LensSection),
|
|
3390
|
+
cards: z.array(LensCard),
|
|
3391
|
+
drill_lenses: z.array(LensMetadata).optional(),
|
|
3392
|
+
alert_triggers: z.array(LensAlertTrigger).optional(),
|
|
3393
|
+
drill_lens_triggers: z.array(LensDrillTrigger).optional()
|
|
3394
|
+
}).loose();
|
|
3395
|
+
z.object({
|
|
3396
|
+
query: DatasetQuery,
|
|
3397
|
+
lens_params: LensParams.nullable().optional()
|
|
3398
|
+
}).strict();
|
|
3399
|
+
//#endregion
|
|
3400
|
+
//#region ../client/src/resources/transform-inspector.ts
|
|
3401
|
+
function lensPath(id, lensId) {
|
|
3402
|
+
return `/api/ee/transforms/${id}/inspect/${encodeURIComponent(lensId)}`;
|
|
3403
|
+
}
|
|
3404
|
+
function transformInspectorResource(transport) {
|
|
3405
|
+
/**
|
|
3406
|
+
* Discover what the inspector can show for a transform: its source and target tables with field
|
|
3407
|
+
* statistics, the fields its query touches, and the lenses available on it.
|
|
3408
|
+
*/
|
|
3409
|
+
async function discover(id, options = {}) {
|
|
3410
|
+
await transport.require("transformInspector.discover", options);
|
|
3411
|
+
return transport.requestParsed(TransformInspection, `/api/ee/transforms/${id}/inspect`, { ...options });
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* Open one lens on a transform: its sections, the cards with the queries that fill them, and
|
|
3415
|
+
* the alert and drill-lens triggers to evaluate against the card results. A drill lens takes
|
|
3416
|
+
* the `params` its trigger emitted.
|
|
3417
|
+
*/
|
|
3418
|
+
async function lens(id, lensId, params = {}, options = {}) {
|
|
3419
|
+
await transport.require("transformInspector.lens", options);
|
|
3420
|
+
return transport.requestParsed(TransformLens, lensPath(id, lensId), {
|
|
3421
|
+
...options,
|
|
3422
|
+
query: { join_step: params.join_step }
|
|
3423
|
+
});
|
|
3424
|
+
}
|
|
3425
|
+
/**
|
|
3426
|
+
* Run a lens card's query in the context of the lens, so the execution is attributed to the
|
|
3427
|
+
* inspector. The body is the card's `dataset_query`, and the same `lens_params` the lens was
|
|
3428
|
+
* opened with.
|
|
3429
|
+
*/
|
|
3430
|
+
async function query(id, lensId, params, options = {}) {
|
|
3431
|
+
await transport.require("transformInspector.query", options);
|
|
3432
|
+
return transport.requestParsed(CardQueryResult, `${lensPath(id, lensId)}/query`, {
|
|
3433
|
+
...options,
|
|
3434
|
+
method: "POST",
|
|
3435
|
+
body: params
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
return {
|
|
3439
|
+
discover,
|
|
3440
|
+
lens,
|
|
3441
|
+
query
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
//#endregion
|
|
3445
|
+
//#region ../client/src/resources/transform-job.ts
|
|
3446
|
+
function transformJobResource(transport) {
|
|
3447
|
+
/** List every transform job the caller can see. */
|
|
3448
|
+
async function list(options = {}) {
|
|
3449
|
+
await transport.require("transformJob.list", options);
|
|
3450
|
+
const { features } = await transport.server(options);
|
|
3451
|
+
return {
|
|
3452
|
+
data: await transport.requestParsed(z.array(transformJobSchema(features)), "/api/transform-job", { ...options }),
|
|
3453
|
+
total: null
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
/** Get one transform job by id. */
|
|
3457
|
+
async function get(id, options = {}) {
|
|
3458
|
+
await transport.require("transformJob.get", options);
|
|
3459
|
+
const { features } = await transport.server(options);
|
|
3460
|
+
return transport.requestParsed(transformJobSchema(features), `/api/transform-job/${id}`, { ...options });
|
|
3461
|
+
}
|
|
3462
|
+
/** Create a transform job — a schedule plus the tags it runs — from a full body. */
|
|
3463
|
+
async function create(params, options = {}) {
|
|
3464
|
+
await transport.require("transformJob.create", options);
|
|
3465
|
+
const { features } = await transport.server(options);
|
|
3466
|
+
return transport.requestParsed(transformJobSchema(features), "/api/transform-job", {
|
|
3467
|
+
...options,
|
|
3468
|
+
method: "POST",
|
|
3469
|
+
body: params
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
3472
|
+
/** Update a transform job by id, patching only the fields the body carries. */
|
|
3473
|
+
async function update(id, params, options = {}) {
|
|
3474
|
+
await transport.require("transformJob.update", options);
|
|
3475
|
+
const { features } = await transport.server(options);
|
|
3476
|
+
return transport.requestParsed(transformJobSchema(features), `/api/transform-job/${id}`, {
|
|
3477
|
+
...options,
|
|
3478
|
+
method: "PUT",
|
|
3479
|
+
body: params
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
/** Delete a transform job by id, leaving the transforms it ran untouched. */
|
|
3483
|
+
async function remove(id, options = {}) {
|
|
3484
|
+
await transport.require("transformJob.delete", options);
|
|
3485
|
+
await transport.requestRaw(`/api/transform-job/${id}`, {
|
|
3486
|
+
...options,
|
|
3487
|
+
method: "DELETE",
|
|
3488
|
+
expectContentType: "binary"
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* Start a run of a transform job by id. The job runs every transform carrying one of its tags,
|
|
3493
|
+
* plus those transforms' dependencies; `run_all` re-runs dependencies that are already fresh.
|
|
3494
|
+
*/
|
|
3495
|
+
async function run(id, params = {}, options = {}) {
|
|
3496
|
+
await transport.require("transformJob.run", options);
|
|
3497
|
+
const { features } = await transport.server(options);
|
|
3498
|
+
return transport.requestParsed(transformJobRunResultSchema(features), `/api/transform-job/${id}/run`, {
|
|
3499
|
+
...options,
|
|
3500
|
+
method: "POST",
|
|
3501
|
+
body: { run_all: params.run_all }
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3504
|
+
/** List the transforms a job would execute, resolved from the job's tags. */
|
|
3505
|
+
async function transforms(id, options = {}) {
|
|
3506
|
+
await transport.require("transformJob.transforms", options);
|
|
3507
|
+
const { features } = await transport.server(options);
|
|
3508
|
+
return {
|
|
3509
|
+
data: await transport.requestParsed(z.array(transformRowSchema(features)), `/api/transform-job/${id}/transforms`, { ...options }),
|
|
3510
|
+
total: null
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
/**
|
|
3514
|
+
* Flip the active flag on every transform job at once. Inactive jobs do not run on schedule;
|
|
3515
|
+
* manual runs ignore the flag. The path carries no id and is a different endpoint from `update`.
|
|
3516
|
+
*/
|
|
3517
|
+
async function setActive(active, options = {}) {
|
|
3518
|
+
await transport.require("transformJob.setActive", options);
|
|
3519
|
+
return transport.requestParsed(TransformJobActiveResult, "/api/transform-job/active", {
|
|
3520
|
+
...options,
|
|
3521
|
+
method: "PUT",
|
|
3522
|
+
body: { active }
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
return {
|
|
3526
|
+
list,
|
|
3527
|
+
get,
|
|
3528
|
+
create,
|
|
3529
|
+
update,
|
|
3530
|
+
delete: remove,
|
|
3531
|
+
run,
|
|
3532
|
+
transforms,
|
|
3533
|
+
setActive
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
//#endregion
|
|
3537
|
+
//#region ../client/src/domain/transform-python.ts
|
|
3538
|
+
const PythonLibrary = z.object({
|
|
3539
|
+
path: z.string(),
|
|
3540
|
+
source: z.string().nullable(),
|
|
3541
|
+
created_at: z.string(),
|
|
3542
|
+
updated_at: z.string()
|
|
3543
|
+
}).loose();
|
|
3544
|
+
z.object({ source: z.string() }).strict();
|
|
3545
|
+
const TEST_RUN_ROW_LIMIT = {
|
|
3546
|
+
min: 2,
|
|
3547
|
+
max: 100
|
|
3548
|
+
};
|
|
3549
|
+
const TestRunRowLimit = z.number().int().min(TEST_RUN_ROW_LIMIT.min).max(TEST_RUN_ROW_LIMIT.max);
|
|
3550
|
+
z.object({
|
|
3551
|
+
code: z.string(),
|
|
3552
|
+
source_tables: z.array(TransformSourceTableEntry).min(1),
|
|
3553
|
+
output_row_limit: TestRunRowLimit.optional(),
|
|
3554
|
+
per_input_row_limit: TestRunRowLimit.optional()
|
|
3555
|
+
}).strict();
|
|
3556
|
+
const PythonTestRunOutput = z.object({
|
|
3557
|
+
cols: z.array(z.object({ name: z.string() }).loose()),
|
|
3558
|
+
rows: z.array(z.unknown())
|
|
3559
|
+
});
|
|
3560
|
+
const PythonTestRunError = z.object({ message: z.string() }).loose();
|
|
3561
|
+
const PythonTestRunSucceeded = z.object({
|
|
3562
|
+
outcome: z.literal("succeeded"),
|
|
3563
|
+
logs: z.string(),
|
|
3564
|
+
output: PythonTestRunOutput
|
|
3565
|
+
});
|
|
3566
|
+
const PythonTestRunFailed = z.object({
|
|
3567
|
+
outcome: z.literal("failed"),
|
|
3568
|
+
logs: z.string(),
|
|
3569
|
+
error: PythonTestRunError
|
|
3570
|
+
});
|
|
3571
|
+
z.discriminatedUnion("outcome", [PythonTestRunSucceeded, PythonTestRunFailed]);
|
|
3572
|
+
//#endregion
|
|
3573
|
+
//#region ../client/src/resources/transform-python.ts
|
|
3574
|
+
const PythonApiTestRun = z.union([z.object({
|
|
3575
|
+
logs: z.string(),
|
|
3576
|
+
output: PythonTestRunOutput
|
|
3577
|
+
}).transform(({ logs, output }) => ({
|
|
3578
|
+
outcome: "succeeded",
|
|
3579
|
+
logs,
|
|
3580
|
+
output
|
|
3581
|
+
})), z.object({
|
|
3582
|
+
logs: z.string(),
|
|
3583
|
+
error: PythonTestRunError
|
|
3584
|
+
}).transform(({ logs, error }) => ({
|
|
3585
|
+
outcome: "failed",
|
|
3586
|
+
logs,
|
|
3587
|
+
error
|
|
3588
|
+
}))]);
|
|
3589
|
+
function libraryPath(path) {
|
|
3590
|
+
return `/api/ee/transforms-python/library/${encodeURIComponent(path)}`;
|
|
3591
|
+
}
|
|
3592
|
+
function transformPythonResource(transport) {
|
|
3593
|
+
/** Get the Python library at a path, the user module every Python transform can import. */
|
|
3594
|
+
async function getLibrary(path, options = {}) {
|
|
3595
|
+
await transport.require("transformPython.getLibrary", options);
|
|
3596
|
+
return transport.requestParsed(PythonLibrary, libraryPath(path), { ...options });
|
|
3597
|
+
}
|
|
3598
|
+
/** Replace the source of the Python library at a path, creating the library when none exists. */
|
|
3599
|
+
async function updateLibrary(path, params, options = {}) {
|
|
3600
|
+
await transport.require("transformPython.updateLibrary", options);
|
|
3601
|
+
return transport.requestParsed(PythonLibrary, libraryPath(path), {
|
|
3602
|
+
...options,
|
|
3603
|
+
method: "PUT",
|
|
3604
|
+
body: params
|
|
3605
|
+
});
|
|
3606
|
+
}
|
|
3607
|
+
/**
|
|
3608
|
+
* Evaluate an ad-hoc Python transform on a sample of its source tables, without a target table.
|
|
3609
|
+
* Meant for early feedback on short runs: input, output and time limits apply, and the sample
|
|
3610
|
+
* sizes are capped at 100 rows.
|
|
3611
|
+
*/
|
|
3612
|
+
async function testRun(params, options = {}) {
|
|
3613
|
+
await transport.require("transformPython.testRun", options);
|
|
3614
|
+
return transport.requestParsed(PythonApiTestRun, "/api/ee/transforms-python/test-run", {
|
|
3615
|
+
...options,
|
|
3616
|
+
method: "POST",
|
|
3617
|
+
body: params
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
return {
|
|
3621
|
+
getLibrary,
|
|
3622
|
+
updateLibrary,
|
|
3623
|
+
testRun
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
//#endregion
|
|
3627
|
+
//#region ../client/src/resources/transform.ts
|
|
3628
|
+
function transformListSchema(features) {
|
|
3629
|
+
return z.array(transformRowSchema(features));
|
|
3630
|
+
}
|
|
3631
|
+
const TransformRunKickoff = z.object({
|
|
3632
|
+
message: z.string(),
|
|
3633
|
+
run_id: z.number().int().positive().nullable()
|
|
3634
|
+
});
|
|
3635
|
+
const TransformDagTransformList = z.array(TransformDagTransform);
|
|
3636
|
+
function transformResource(transport) {
|
|
3637
|
+
async function requestTransformList(path, opts) {
|
|
3638
|
+
const { features } = await transport.server(opts);
|
|
3639
|
+
return {
|
|
3640
|
+
data: await transport.requestParsed(transformListSchema(features), path, opts),
|
|
3641
|
+
total: null
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
async function requestTransform(path, opts) {
|
|
3645
|
+
const { features } = await transport.server(opts);
|
|
3646
|
+
return transport.requestParsed(transformRowSchema(features), path, opts);
|
|
3647
|
+
}
|
|
3648
|
+
/** List every transform the caller can see. */
|
|
3649
|
+
async function list(options = {}) {
|
|
3650
|
+
await transport.require("transform.list", options);
|
|
3651
|
+
return requestTransformList("/api/transform", { ...options });
|
|
3652
|
+
}
|
|
3653
|
+
/** Get one transform by id. */
|
|
3654
|
+
async function get(id, options = {}) {
|
|
3655
|
+
await transport.require("transform.get", options);
|
|
3656
|
+
const { features } = await transport.server(options);
|
|
3657
|
+
return transport.requestParsed(transformDetailSchema(features), `/api/transform/${id}`, { ...options });
|
|
3658
|
+
}
|
|
3659
|
+
/** Create a transform — a query plus the warehouse table it writes — from a full body. */
|
|
3660
|
+
async function create(params, options = {}) {
|
|
3661
|
+
await transport.require("transform.create", options);
|
|
3662
|
+
return requestTransform("/api/transform", {
|
|
3663
|
+
...options,
|
|
3664
|
+
method: "POST",
|
|
3665
|
+
body: params
|
|
3666
|
+
});
|
|
3667
|
+
}
|
|
3668
|
+
/** Update a transform by id, patching only the fields the body carries. */
|
|
3669
|
+
async function update(id, params, options = {}) {
|
|
3670
|
+
await transport.require("transform.update", options);
|
|
3671
|
+
return requestTransform(`/api/transform/${id}`, {
|
|
3672
|
+
...options,
|
|
3673
|
+
method: "PUT",
|
|
3674
|
+
body: params
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
/** Delete a transform by id, leaving any table it already materialized in place. */
|
|
3678
|
+
async function remove(id, options = {}) {
|
|
3679
|
+
await transport.require("transform.delete", options);
|
|
3680
|
+
await transport.requestRaw(`/api/transform/${id}`, {
|
|
3681
|
+
...options,
|
|
3682
|
+
method: "DELETE",
|
|
3683
|
+
expectContentType: "binary"
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
/** Drop a transform's materialized output table, keeping the transform definition. */
|
|
3687
|
+
async function deleteTable(id, options = {}) {
|
|
3688
|
+
await transport.require("transform.deleteTable", options);
|
|
3689
|
+
await transport.requestRaw(`/api/transform/${id}/table`, {
|
|
3690
|
+
...options,
|
|
3691
|
+
method: "DELETE",
|
|
3692
|
+
expectContentType: "binary"
|
|
3693
|
+
});
|
|
3694
|
+
}
|
|
3695
|
+
/** List the upstream transforms a transform depends on. */
|
|
3696
|
+
async function dependencies(id, options = {}) {
|
|
3697
|
+
await transport.require("transform.dependencies", options);
|
|
3698
|
+
return requestTransformList(`/api/transform/${id}/dependencies`, { ...options });
|
|
3699
|
+
}
|
|
3700
|
+
/** Request cancellation of a transform's current run. */
|
|
3701
|
+
async function cancel(id, options = {}) {
|
|
3702
|
+
await transport.require("transform.cancel", options);
|
|
3703
|
+
await transport.requestRaw(`/api/transform/${id}/cancel`, {
|
|
3704
|
+
...options,
|
|
3705
|
+
method: "POST",
|
|
3706
|
+
expectContentType: "binary"
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
/** Get one transform run by run id — not by the id of the transform that produced it. */
|
|
3710
|
+
async function getRun(runId, options = {}) {
|
|
3711
|
+
await transport.require("transform.getRun", options);
|
|
3712
|
+
return transport.requestParsed(TransformRun, `/api/transform/run/${runId}`, { ...options });
|
|
3713
|
+
}
|
|
3714
|
+
/** Walk the run history, newest first, optionally narrowed to a single transform. */
|
|
3715
|
+
async function* runPages(params = {}, options = {}) {
|
|
3716
|
+
await transport.require("transform.runPages", options);
|
|
3717
|
+
yield* paginatePages(transport, "/api/transform/run", TransformRun, {
|
|
3718
|
+
query: { "transform-ids": params["transform-ids"] },
|
|
3719
|
+
...options.offset !== void 0 && { offset: options.offset },
|
|
3720
|
+
...options.max !== void 0 && { max: options.max },
|
|
3721
|
+
...options.pageSize !== void 0 && { pageSize: options.pageSize },
|
|
3722
|
+
...options.signal !== void 0 && { signal: options.signal }
|
|
3723
|
+
});
|
|
3724
|
+
}
|
|
3725
|
+
/**
|
|
3726
|
+
* Walk the unified run history, newest first by default: every row is a root run — a job run, a
|
|
3727
|
+
* DAG reprocess run, or a standalone transform run — and never a member run of a job or DAG.
|
|
3728
|
+
*/
|
|
3729
|
+
async function* runSummaryPages(params = {}, options = {}) {
|
|
3730
|
+
await transport.require("transform.runSummaryPages", options);
|
|
3731
|
+
yield* paginatePages(transport, "/api/transform/runs", TransformRunSummary, {
|
|
3732
|
+
query: {
|
|
3733
|
+
types: params.types,
|
|
3734
|
+
statuses: params.statuses,
|
|
3735
|
+
"run-methods": params["run-methods"],
|
|
3736
|
+
"start-time": params["start-time"],
|
|
3737
|
+
"end-time": params["end-time"],
|
|
3738
|
+
"transform-ids": params["transform-ids"],
|
|
3739
|
+
"sort-column": params["sort-column"],
|
|
3740
|
+
"sort-direction": params["sort-direction"]
|
|
3741
|
+
},
|
|
3742
|
+
...options.offset !== void 0 && { offset: options.offset },
|
|
3743
|
+
...options.max !== void 0 && { max: options.max },
|
|
3744
|
+
...options.pageSize !== void 0 && { pageSize: options.pageSize },
|
|
3745
|
+
...options.signal !== void 0 && { signal: options.signal }
|
|
3746
|
+
});
|
|
3747
|
+
}
|
|
3748
|
+
/**
|
|
3749
|
+
* Reset the stored checkpoint of an incremental transform, so its next run reprocesses the whole
|
|
3750
|
+
* source rather than the rows past the last checkpoint.
|
|
3751
|
+
*/
|
|
3752
|
+
async function resetCheckpoint(id, options = {}) {
|
|
3753
|
+
await transport.require("transform.resetCheckpoint", options);
|
|
3754
|
+
await transport.requestRaw(`/api/transform/${id}/reset-checkpoint`, {
|
|
3755
|
+
...options,
|
|
3756
|
+
method: "POST",
|
|
3757
|
+
expectContentType: "binary"
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
/**
|
|
3761
|
+
* Start a DAG reprocess run seeded at a transform: the transform plus, by `direction`, every
|
|
3762
|
+
* transform it transitively depends on or every transform that transitively depends on it. The
|
|
3763
|
+
* server answers at once with the run's id, or null when it ran nothing because a DAG run for
|
|
3764
|
+
* this transform is already in progress or the closure is empty. Progress is read off
|
|
3765
|
+
* `runSummaryPages` with `types: ["dag"]`, and the member runs off `transformDagRun.transformRuns`.
|
|
3766
|
+
*/
|
|
3767
|
+
async function runDag(id, params, options = {}) {
|
|
3768
|
+
await transport.require("transform.runDag", options);
|
|
3769
|
+
return transport.requestParsed(TransformDagRunResult, `/api/transform/${id}/run-dag`, {
|
|
3770
|
+
...options,
|
|
3771
|
+
method: "POST",
|
|
3772
|
+
body: { direction: params.direction }
|
|
3773
|
+
});
|
|
3774
|
+
}
|
|
3775
|
+
/** Preview the transforms a DAG reprocess seeded at a transform would run, in execution order. */
|
|
3776
|
+
async function dagTransforms(id, params, options = {}) {
|
|
3777
|
+
await transport.require("transform.dagTransforms", options);
|
|
3778
|
+
return {
|
|
3779
|
+
data: await transport.requestParsed(TransformDagTransformList, `/api/transform/${id}/dag-transforms`, {
|
|
3780
|
+
...options,
|
|
3781
|
+
query: { direction: params.direction }
|
|
3782
|
+
}),
|
|
3783
|
+
total: null
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
/**
|
|
3787
|
+
* Start a run of a transform by id. The server queues the run and answers at once; `wait` polls
|
|
3788
|
+
* it to a terminal status, and `syncTarget` follows that with the output table's registration.
|
|
3789
|
+
* A server that answers no run id started nothing, and there is then nothing to poll.
|
|
3790
|
+
*/
|
|
3791
|
+
async function run(id, params = {}, options = {}) {
|
|
3792
|
+
await transport.require("transform.run", options);
|
|
3793
|
+
const kickoff = await transport.requestParsed(TransformRunKickoff, `/api/transform/${id}/run`, {
|
|
3794
|
+
...options,
|
|
3795
|
+
method: "POST"
|
|
3796
|
+
});
|
|
3797
|
+
const schedule = params.syncTarget === true ? params.wait ?? {} : params.wait;
|
|
3798
|
+
if (schedule === void 0 || kickoff.run_id === null) return {
|
|
3799
|
+
message: kickoff.message,
|
|
3800
|
+
run_id: kickoff.run_id,
|
|
3801
|
+
final: null
|
|
3802
|
+
};
|
|
3803
|
+
const runId = kickoff.run_id;
|
|
3804
|
+
const final = await pollUntil(async (signal) => getRun(runId, {
|
|
3805
|
+
...options,
|
|
3806
|
+
signal
|
|
3807
|
+
}), (candidate) => isTransformRunTerminal(candidate.status), schedule);
|
|
3808
|
+
if (params.syncTarget !== true) return {
|
|
3809
|
+
message: kickoff.message,
|
|
3810
|
+
run_id: runId,
|
|
3811
|
+
final
|
|
3812
|
+
};
|
|
3813
|
+
const targetTableId = isTransformRunFailed(final.status) ? null : await awaitTargetTableId(id, schedule, options);
|
|
3814
|
+
return {
|
|
3815
|
+
message: kickoff.message,
|
|
3816
|
+
run_id: runId,
|
|
3817
|
+
final,
|
|
3818
|
+
target_table_id: targetTableId
|
|
3819
|
+
};
|
|
3820
|
+
}
|
|
3821
|
+
async function awaitTargetTableId(id, wait, options) {
|
|
3822
|
+
try {
|
|
3823
|
+
return (await pollUntil(async (signal) => get(id, {
|
|
3824
|
+
...options,
|
|
3825
|
+
signal
|
|
3826
|
+
}), (transform) => transform.target_table_id !== null, wait)).target_table_id;
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
if (error instanceof TimeoutError) return null;
|
|
3829
|
+
throw error;
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
return {
|
|
3833
|
+
list,
|
|
3834
|
+
get,
|
|
3835
|
+
create,
|
|
3836
|
+
update,
|
|
3837
|
+
delete: remove,
|
|
3838
|
+
deleteTable,
|
|
3839
|
+
dependencies,
|
|
3840
|
+
cancel,
|
|
3841
|
+
getRun,
|
|
3842
|
+
runPages,
|
|
3843
|
+
runSummaryPages,
|
|
3844
|
+
resetCheckpoint,
|
|
3845
|
+
runDag,
|
|
3846
|
+
dagTransforms,
|
|
3847
|
+
run
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
//#endregion
|
|
3851
|
+
//#region ../client/src/resources/transform-tag.ts
|
|
3852
|
+
const TransformTagApiList = z.array(TransformTag);
|
|
3853
|
+
function transformTagResource(transport) {
|
|
3854
|
+
/** List every transform tag the caller can see, built-in tags included. */
|
|
3855
|
+
async function list(options = {}) {
|
|
3856
|
+
await transport.require("transformTag.list", options);
|
|
3857
|
+
return {
|
|
3858
|
+
data: await transport.requestParsed(TransformTagApiList, "/api/transform-tag", { ...options }),
|
|
3859
|
+
total: null
|
|
3860
|
+
};
|
|
3861
|
+
}
|
|
3862
|
+
/** Create a transform tag, the label a transform job matches transforms on. */
|
|
3863
|
+
async function create(params, options = {}) {
|
|
3864
|
+
await transport.require("transformTag.create", options);
|
|
3865
|
+
return transport.requestParsed(TransformTag, "/api/transform-tag", {
|
|
3866
|
+
...options,
|
|
3867
|
+
method: "POST",
|
|
3868
|
+
body: params
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
/** Rename a transform tag by id. */
|
|
3872
|
+
async function update(id, params, options = {}) {
|
|
3873
|
+
await transport.require("transformTag.update", options);
|
|
3874
|
+
return transport.requestParsed(TransformTag, `/api/transform-tag/${id}`, {
|
|
3875
|
+
...options,
|
|
3876
|
+
method: "PUT",
|
|
3877
|
+
body: params
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
/** Delete a transform tag by id, detaching it from every transform and job carrying it. */
|
|
3881
|
+
async function remove(id, options = {}) {
|
|
3882
|
+
await transport.require("transformTag.delete", options);
|
|
3883
|
+
await transport.requestRaw(`/api/transform-tag/${id}`, {
|
|
3884
|
+
...options,
|
|
3885
|
+
method: "DELETE",
|
|
3886
|
+
expectContentType: "binary"
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
return {
|
|
3890
|
+
list,
|
|
3891
|
+
create,
|
|
3892
|
+
update,
|
|
3893
|
+
delete: remove
|
|
3894
|
+
};
|
|
3895
|
+
}
|
|
3896
|
+
//#endregion
|
|
3897
|
+
//#region ../client/src/resources/transform-test.ts
|
|
3898
|
+
const TransformTestApiList = z.array(TransformTest);
|
|
3899
|
+
function transformTestResource(transport) {
|
|
3900
|
+
/** List the transform tests, optionally only those of one transform. */
|
|
3901
|
+
async function list(params = {}, options = {}) {
|
|
3902
|
+
await transport.require("transformTest.list", options);
|
|
3903
|
+
return {
|
|
3904
|
+
data: await transport.requestParsed(TransformTestApiList, "/api/ee/transform-test", {
|
|
3905
|
+
...options,
|
|
3906
|
+
query: { "transform-id": params["transform-id"] }
|
|
3907
|
+
}),
|
|
3908
|
+
total: null
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
/** Get a transform test by id. */
|
|
3912
|
+
async function get(id, options = {}) {
|
|
3913
|
+
await transport.require("transformTest.get", options);
|
|
3914
|
+
return transport.requestParsed(TransformTest, `/api/ee/transform-test/${id}`, { ...options });
|
|
3915
|
+
}
|
|
3916
|
+
/**
|
|
3917
|
+
* Create a transform test. The server validates the inputs and expectations against the
|
|
3918
|
+
* transform before saving, and refuses with an `HttpError` whose `errorCode` is a
|
|
3919
|
+
* `TransformTestRefusalCode` when they cannot run.
|
|
3920
|
+
*/
|
|
3921
|
+
async function create(params, options = {}) {
|
|
3922
|
+
await transport.require("transformTest.create", options);
|
|
3923
|
+
return transport.requestParsed(TransformTest, "/api/ee/transform-test", {
|
|
3924
|
+
...options,
|
|
3925
|
+
method: "POST",
|
|
3926
|
+
body: params
|
|
3927
|
+
});
|
|
3928
|
+
}
|
|
3929
|
+
/**
|
|
3930
|
+
* Update a transform test by id, leaving omitted fields unchanged. A body that touches the
|
|
3931
|
+
* transform, inputs or expectations is validated as on create, with the same refusal.
|
|
3932
|
+
*/
|
|
3933
|
+
async function update(id, params, options = {}) {
|
|
3934
|
+
await transport.require("transformTest.update", options);
|
|
3935
|
+
return transport.requestParsed(TransformTest, `/api/ee/transform-test/${id}`, {
|
|
3936
|
+
...options,
|
|
3937
|
+
method: "PUT",
|
|
3938
|
+
body: params
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
/** Delete a transform test by id. */
|
|
3942
|
+
async function remove(id, options = {}) {
|
|
3943
|
+
await transport.require("transformTest.delete", options);
|
|
3944
|
+
await transport.requestRaw(`/api/ee/transform-test/${id}`, {
|
|
3945
|
+
...options,
|
|
3946
|
+
method: "DELETE",
|
|
3947
|
+
expectContentType: "binary"
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
/**
|
|
3951
|
+
* Run a transform test by id. A result means the run happened: `status` is `passed` or `failed`
|
|
3952
|
+
* and every expectation reports what it found, one that could not be evaluated as its own
|
|
3953
|
+
* `error`. An `HttpError` whose `errorCode` is a `TransformTestRefusalCode` means the run was
|
|
3954
|
+
* refused and nothing meaningful ran.
|
|
3955
|
+
*/
|
|
3956
|
+
async function run(id, options = {}) {
|
|
3957
|
+
await transport.require("transformTest.run", options);
|
|
3958
|
+
return transport.requestParsed(TransformTestRunResult, `/api/ee/transform-test/${id}/run`, {
|
|
3959
|
+
...options,
|
|
3960
|
+
method: "POST"
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
return {
|
|
3964
|
+
list,
|
|
3965
|
+
get,
|
|
3966
|
+
create,
|
|
3967
|
+
update,
|
|
3968
|
+
delete: remove,
|
|
3969
|
+
run
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
//#endregion
|
|
3973
|
+
//#region ../client/src/resources/upload.ts
|
|
3974
|
+
const UPLOAD_TABLE_ID_HEADER = "metabase-table-id";
|
|
3975
|
+
function uploadResource(transport) {
|
|
3976
|
+
/**
|
|
3977
|
+
* Create a table and model populated with the values from the attached CSV. Returns the model ID
|
|
3978
|
+
* if successful.
|
|
3979
|
+
*
|
|
3980
|
+
* The file may be at most 50 MB; larger uploads are rejected with a 413 response.
|
|
3981
|
+
*/
|
|
3982
|
+
async function createFromCsv(file, params, options = {}) {
|
|
3983
|
+
await transport.require("upload.createFromCsv", options);
|
|
3984
|
+
const form = buildCsvFormData(file);
|
|
3985
|
+
form.append("collection_id", params.collection_id);
|
|
3986
|
+
const response = await transport.requestRaw("/api/upload/csv", {
|
|
3987
|
+
...options,
|
|
3988
|
+
method: "POST",
|
|
3989
|
+
body: form,
|
|
3990
|
+
expectContentType: "binary"
|
|
3991
|
+
});
|
|
3992
|
+
return parseCreateUploadResult(await response.text(), response.headers);
|
|
3993
|
+
}
|
|
3994
|
+
return { createFromCsv };
|
|
3995
|
+
}
|
|
3996
|
+
function parseCreateUploadResult(bodyText, headers) {
|
|
3997
|
+
return UploadResult.parse({
|
|
3998
|
+
model_id: parseResponseInteger(bodyText, "response body"),
|
|
3999
|
+
table_id: parseResponseInteger(headers.get(UPLOAD_TABLE_ID_HEADER), `${UPLOAD_TABLE_ID_HEADER} header`)
|
|
4000
|
+
});
|
|
4001
|
+
}
|
|
4002
|
+
function parseResponseInteger(value, source) {
|
|
4003
|
+
const trimmed = value?.trim() ?? "";
|
|
4004
|
+
if (trimmed === "") throw new ResponseShapeError(`upload succeeded but the ${source} was empty`, {
|
|
4005
|
+
kind: "decoded",
|
|
4006
|
+
source,
|
|
4007
|
+
value
|
|
4008
|
+
});
|
|
4009
|
+
const parsed = Number(trimmed);
|
|
4010
|
+
if (!Number.isInteger(parsed)) throw new ResponseShapeError(`upload succeeded but the ${source} was not an integer: ${JSON.stringify(trimmed)}`, {
|
|
4011
|
+
kind: "decoded",
|
|
4012
|
+
source,
|
|
4013
|
+
value
|
|
4014
|
+
});
|
|
4015
|
+
return parsed;
|
|
4016
|
+
}
|
|
4017
|
+
//#endregion
|
|
4018
|
+
//#region ../client/src/domain/user.ts
|
|
4019
|
+
const CurrentUser = z.object({
|
|
4020
|
+
id: z.number().int(),
|
|
4021
|
+
email: z.email(),
|
|
4022
|
+
common_name: z.string(),
|
|
4023
|
+
is_superuser: z.boolean()
|
|
4024
|
+
}).loose();
|
|
4025
|
+
CurrentUser.pick({
|
|
4026
|
+
id: true,
|
|
4027
|
+
email: true,
|
|
4028
|
+
common_name: true,
|
|
4029
|
+
is_superuser: true
|
|
4030
|
+
}).strip();
|
|
4031
|
+
//#endregion
|
|
4032
|
+
//#region ../client/src/resources/user.ts
|
|
4033
|
+
function userResource(transport) {
|
|
4034
|
+
/** Get the user the request's credentials authenticate as. */
|
|
4035
|
+
async function current(options = {}) {
|
|
4036
|
+
await transport.require("user.current", options);
|
|
4037
|
+
return transport.requestParsed(CurrentUser, "/api/user/current", { ...options });
|
|
4038
|
+
}
|
|
4039
|
+
return { current };
|
|
4040
|
+
}
|
|
4041
|
+
//#endregion
|
|
4042
|
+
//#region ../client/src/client.ts
|
|
4043
|
+
var client_exports = /* @__PURE__ */ __exportAll({ createClient: () => createClient });
|
|
4044
|
+
function createClient(config, options) {
|
|
4045
|
+
const transport = createTransport(config, options);
|
|
4046
|
+
return {
|
|
4047
|
+
card: cardResource(transport),
|
|
4048
|
+
collection: collectionResource(transport),
|
|
4049
|
+
contentTranslation: contentTranslationResource(transport),
|
|
4050
|
+
dashboard: dashboardResource(transport),
|
|
4051
|
+
database: databaseResource(transport),
|
|
4052
|
+
dataset: datasetResource(transport),
|
|
4053
|
+
dependency: dependencyResource(transport),
|
|
4054
|
+
document: documentResource(transport),
|
|
4055
|
+
eidTranslation: eidTranslationResource(transport),
|
|
4056
|
+
erd: erdResource(transport),
|
|
4057
|
+
field: fieldResource(transport),
|
|
4058
|
+
gitSync: gitSyncResource(transport),
|
|
4059
|
+
glossary: glossaryResource(transport),
|
|
4060
|
+
library: libraryResource(transport),
|
|
4061
|
+
measure: measureResource(transport),
|
|
4062
|
+
metric: metricResource(transport),
|
|
4063
|
+
moderationReview: moderationReviewResource(transport),
|
|
4064
|
+
notification: notificationResource(transport),
|
|
4065
|
+
permission: permissionResource(transport),
|
|
4066
|
+
pulse: pulseResource(transport),
|
|
4067
|
+
replacement: replacementResource(transport),
|
|
4068
|
+
revision: revisionResource(transport),
|
|
4069
|
+
search: searchResource(transport),
|
|
4070
|
+
segment: segmentResource(transport),
|
|
4071
|
+
setting: settingResource(transport),
|
|
4072
|
+
setup: setupResource(transport),
|
|
4073
|
+
snippet: snippetResource(transport),
|
|
4074
|
+
table: tableResource(transport),
|
|
4075
|
+
timeline: timelineResource(transport),
|
|
4076
|
+
timelineEvent: timelineEventResource(transport),
|
|
4077
|
+
transform: transformResource(transport),
|
|
4078
|
+
transformDagRun: transformDagRunResource(transport),
|
|
4079
|
+
transformInspector: transformInspectorResource(transport),
|
|
4080
|
+
transformJob: transformJobResource(transport),
|
|
4081
|
+
transformPython: transformPythonResource(transport),
|
|
4082
|
+
transformTag: transformTagResource(transport),
|
|
4083
|
+
transformTest: transformTestResource(transport),
|
|
4084
|
+
upload: uploadResource(transport),
|
|
4085
|
+
user: userResource(transport),
|
|
4086
|
+
server: transport.server,
|
|
4087
|
+
requestParsed: transport.requestParsed,
|
|
4088
|
+
requestRaw: transport.requestRaw,
|
|
4089
|
+
requestStream: transport.requestStream
|
|
4090
|
+
};
|
|
4091
|
+
}
|
|
4092
|
+
//#endregion
|
|
4093
|
+
export { createClient as n, client_exports as t };
|