@prisma/orm-toolchain 8.0.0-rc.1-dev.45 → 8.0.0-rc.1-dev.46
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/README.md +3 -3
- package/dist/{aggregate-BBsCbXgJ.mjs → aggregate-CPZB020V.mjs} +9 -9
- package/dist/{aggregate-BBsCbXgJ.mjs.map → aggregate-CPZB020V.mjs.map} +1 -1
- package/dist/{aggregate-CA6bJ8UO.d.mts → aggregate-DLUJLq5Y.d.mts} +3 -3
- package/dist/{aggregate-CA6bJ8UO.d.mts.map → aggregate-DLUJLq5Y.d.mts.map} +1 -1
- package/dist/cli-telemetry.d.mts +19 -5
- package/dist/cli-telemetry.d.mts.map +1 -1
- package/dist/cli-telemetry.mjs +287 -8
- package/dist/cli-telemetry.mjs.map +1 -1
- package/dist/cli.d.mts +11 -13
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +5382 -1095
- package/dist/cli.mjs.map +1 -1
- package/dist/cli__control-api.d.mts +298 -27
- package/dist/cli__control-api.d.mts.map +1 -1
- package/dist/cli__control-api.mjs +2 -19
- package/dist/cli__control-api__testing.d.mts +1 -1
- package/dist/cli__control-api__testing.mjs.map +1 -1
- package/dist/cli__init-output.d.mts +0 -1
- package/dist/cli__init-output.d.mts.map +1 -1
- package/dist/cli__init-output.mjs +1 -1
- package/dist/cli__migration-cli.mjs +9 -9
- package/dist/cli__migration-cli.mjs.map +1 -1
- package/dist/config-loader.d.mts +1 -1
- package/dist/config-loader.mjs +1 -1
- package/dist/{contract-snapshot-store-B0G1Fw_F-C4JjmWEA.mjs → contract-snapshot-store-B0G1Fw_F-CHc4QfKb.mjs} +4 -4
- package/dist/{contract-snapshot-store-B0G1Fw_F-C4JjmWEA.mjs.map → contract-snapshot-store-B0G1Fw_F-CHc4QfKb.mjs.map} +1 -1
- package/dist/{cli-errors-iquUoY6O-BFboxglv.d.mts → db-verify-BwgNK50T-DGvKgFCK.d.mts} +81 -81
- package/dist/db-verify-BwgNK50T-DGvKgFCK.d.mts.map +1 -0
- package/dist/emitter.d.mts +1 -1
- package/dist/{errors-CXbGUWGu-BQi6YS-H.mjs → errors-CXbGUWGu-CstQHPh6.mjs} +2 -2
- package/dist/{errors-CXbGUWGu-BQi6YS-H.mjs.map → errors-CXbGUWGu-CstQHPh6.mjs.map} +1 -1
- package/dist/{errors-KWM8yBAL.d.mts → errors-D59OCpoI.d.mts} +2 -2
- package/dist/{errors-KWM8yBAL.d.mts.map → errors-D59OCpoI.d.mts.map} +1 -1
- package/dist/{exports-B3xY_FHp.mjs → exports-8ZUN43ve.mjs} +6 -6
- package/dist/exports-8ZUN43ve.mjs.map +1 -0
- package/dist/{exports-BZLNbtkY.mjs → exports-BZaG8_w_.mjs} +64 -36
- package/dist/exports-BZaG8_w_.mjs.map +1 -0
- package/dist/{graph-membership-BViWUMpP-Cgu4Z2ox.mjs → graph-membership-BViWUMpP-C4lYYJdU.mjs} +2 -2
- package/dist/{graph-membership-BViWUMpP-Cgu4Z2ox.mjs.map → graph-membership-BViWUMpP-C4lYYJdU.mjs.map} +1 -1
- package/dist/{import-roots-NDOIEDVj.mjs → import-roots-CNFBPAT1.mjs} +10 -3
- package/dist/{import-roots-NDOIEDVj.mjs.map → import-roots-CNFBPAT1.mjs.map} +1 -1
- package/dist/{import-roots-B6CtsWXb.d.mts → import-roots-CfoYgejL.d.mts} +2 -2
- package/dist/{import-roots-B6CtsWXb.d.mts.map → import-roots-CfoYgejL.d.mts.map} +1 -1
- package/dist/{index-mCsi_ND5.d.mts → index-DRs7L6N0.d.mts} +11 -3
- package/dist/index-DRs7L6N0.d.mts.map +1 -0
- package/dist/{index-CnOyYxmv.d.mts → index-Dgwl8Tzs.d.mts} +2 -2
- package/dist/{index-CnOyYxmv.d.mts.map → index-Dgwl8Tzs.d.mts.map} +1 -1
- package/dist/{invariants-CTHy88OP-DJ-i5yTz.mjs → invariants-CTHy88OP-CYI2a8IO.mjs} +2 -2
- package/dist/{invariants-CTHy88OP-DJ-i5yTz.mjs.map → invariants-CTHy88OP-CYI2a8IO.mjs.map} +1 -1
- package/dist/{io-635GlNx1-D94PxRc-.mjs → io-635GlNx1-D3zrw2qu.mjs} +4 -4
- package/dist/{io-635GlNx1-D94PxRc-.mjs.map → io-635GlNx1-D3zrw2qu.mjs.map} +1 -1
- package/dist/{migration-graph-_SJImD5V-D5RCc651.mjs → migration-graph-_SJImD5V-BURyY1KM.mjs} +2 -2
- package/dist/{migration-graph-_SJImD5V-D5RCc651.mjs.map → migration-graph-_SJImD5V-BURyY1KM.mjs.map} +1 -1
- package/dist/{migration-gubSSeMk.mjs → migration-hspPIEeV.mjs} +4 -4
- package/dist/{migration-gubSSeMk.mjs.map → migration-hspPIEeV.mjs.map} +1 -1
- package/dist/migration-tools.d.mts +4 -4
- package/dist/migration-tools.mjs +13 -13
- package/dist/migration-tools__aggregate.d.mts +1 -1
- package/dist/migration-tools__aggregate.mjs +1 -1
- package/dist/migration-tools__contract-snapshot-store.mjs +1 -1
- package/dist/migration-tools__errors.d.mts +1 -1
- package/dist/migration-tools__errors.mjs +1 -1
- package/dist/migration-tools__invariants.mjs +1 -1
- package/dist/migration-tools__io.mjs +1 -1
- package/dist/migration-tools__migration-graph.mjs +2 -2
- package/dist/migration-tools__migration-ts.mjs +1 -1
- package/dist/migration-tools__migration.mjs +1 -1
- package/dist/migration-tools__ref-resolution.mjs +1 -1
- package/dist/migration-tools__refs.mjs +1 -1
- package/dist/migration-tools__spaces.d.mts +2 -2
- package/dist/migration-tools__spaces.mjs +2 -2
- package/dist/{migration-ts-BoHxxGvi.mjs → migration-ts-DdVm0rws.mjs} +2 -2
- package/dist/{migration-ts-BoHxxGvi.mjs.map → migration-ts-DdVm0rws.mjs.map} +1 -1
- package/dist/{output-DP3woAjV-BxCr9nHA.mjs → output-CovTDY14-CUoAqLML.mjs} +3 -45
- package/dist/output-CovTDY14-CUoAqLML.mjs.map +1 -0
- package/dist/publish-surface.d.mts +2 -2
- package/dist/publish-surface.mjs +2 -2
- package/dist/publish-surface__import-roots.d.mts +1 -1
- package/dist/publish-surface__import-roots.mjs +1 -1
- package/dist/publish-surface__shells.d.mts +1 -1
- package/dist/publish-surface__shells.mjs +1 -1
- package/dist/quick-reference-mongo.md +12 -9
- package/dist/quick-reference-postgres.md +12 -9
- package/dist/readme-mongo.md +2 -2
- package/dist/readme-postgres.md +2 -2
- package/dist/ref-h6InR6dO-rGfl18Jv.mjs +4686 -0
- package/dist/ref-h6InR6dO-rGfl18Jv.mjs.map +1 -0
- package/dist/{ref-resolution-sGcINe7o.mjs → ref-resolution-HeNBXW53.mjs} +2 -2
- package/dist/{ref-resolution-sGcINe7o.mjs.map → ref-resolution-HeNBXW53.mjs.map} +1 -1
- package/dist/{refs-WK86uYYp-B6EZkmmT.mjs → refs-WK86uYYp-gM8tvWQP.mjs} +2 -2
- package/dist/{refs-WK86uYYp-B6EZkmmT.mjs.map → refs-WK86uYYp-gM8tvWQP.mjs.map} +1 -1
- package/dist/{shells-BenrJHp2.mjs → shells-C4HbRAr5.mjs} +13 -14
- package/dist/shells-C4HbRAr5.mjs.map +1 -0
- package/dist/{shells-COHV_Ozf.d.mts → shells-CIUmT4u4.d.mts} +14 -20
- package/dist/shells-CIUmT4u4.d.mts.map +1 -0
- package/dist/{spaces-BG2w0P8d.mjs → spaces-4qMOAYE9.mjs} +7 -7
- package/dist/{spaces-BG2w0P8d.mjs.map → spaces-4qMOAYE9.mjs.map} +1 -1
- package/dist/{spaces-tWQi7CUv.d.mts → spaces-Dj65JrwZ.d.mts} +2 -2
- package/dist/{spaces-tWQi7CUv.d.mts.map → spaces-Dj65JrwZ.d.mts.map} +1 -1
- package/dist/{verify-contract-spaces-Cc1u_wJo-BilPJS2S.d.mts → verify-contract-spaces-C3PiFBk3-DfomYyOE.d.mts} +3 -3
- package/dist/{verify-contract-spaces-Cc1u_wJo-BilPJS2S.d.mts.map → verify-contract-spaces-C3PiFBk3-DfomYyOE.d.mts.map} +1 -1
- package/dist/{verify-contract-spaces-DhJTb0zM-_oh6W6lp.mjs → verify-contract-spaces-DhJTb0zM-B62maCfE.mjs} +4 -4
- package/dist/{verify-contract-spaces-DhJTb0zM-_oh6W6lp.mjs.map → verify-contract-spaces-DhJTb0zM-B62maCfE.mjs.map} +1 -1
- package/dist/vite-plugin-contract-emit.d.mts +2 -2
- package/dist/vite-plugin-contract-emit.mjs +5 -5
- package/dist/vite-plugin-contract-emit.mjs.map +1 -1
- package/package.json +14 -38
- package/dist/bin__prisma-next.mjs +0 -512
- package/dist/bin__prisma-next.mjs.map +0 -1
- package/dist/caught-errors-BZo7zcRc-C-pIEasG.mjs +0 -14
- package/dist/caught-errors-BZo7zcRc-C-pIEasG.mjs.map +0 -1
- package/dist/cli-errors-B4zTLaqQ-CXgjO9t4.mjs +0 -347
- package/dist/cli-errors-B4zTLaqQ-CXgjO9t4.mjs.map +0 -1
- package/dist/cli-errors-iquUoY6O-BFboxglv.d.mts.map +0 -1
- package/dist/cli__commands__contract-emit.d.mts +0 -2
- package/dist/cli__commands__contract-emit.mjs +0 -2
- package/dist/cli__commands__contract-infer.d.mts +0 -7
- package/dist/cli__commands__contract-infer.d.mts.map +0 -1
- package/dist/cli__commands__contract-infer.mjs +0 -2
- package/dist/cli__commands__db-init.d.mts +0 -7
- package/dist/cli__commands__db-init.d.mts.map +0 -1
- package/dist/cli__commands__db-init.mjs +0 -2
- package/dist/cli__commands__db-schema.d.mts +0 -7
- package/dist/cli__commands__db-schema.d.mts.map +0 -1
- package/dist/cli__commands__db-schema.mjs +0 -2
- package/dist/cli__commands__db-sign.d.mts +0 -7
- package/dist/cli__commands__db-sign.d.mts.map +0 -1
- package/dist/cli__commands__db-sign.mjs +0 -2
- package/dist/cli__commands__db-update.d.mts +0 -7
- package/dist/cli__commands__db-update.d.mts.map +0 -1
- package/dist/cli__commands__db-update.mjs +0 -2
- package/dist/cli__commands__db-verify.d.mts +0 -7
- package/dist/cli__commands__db-verify.d.mts.map +0 -1
- package/dist/cli__commands__db-verify.mjs +0 -2
- package/dist/cli__commands__migrate.d.mts +0 -57
- package/dist/cli__commands__migrate.d.mts.map +0 -1
- package/dist/cli__commands__migrate.mjs +0 -2
- package/dist/cli__commands__migration-check.d.mts +0 -8
- package/dist/cli__commands__migration-check.d.mts.map +0 -1
- package/dist/cli__commands__migration-check.mjs +0 -3
- package/dist/cli__commands__migration-graph.d.mts +0 -38
- package/dist/cli__commands__migration-graph.d.mts.map +0 -1
- package/dist/cli__commands__migration-graph.mjs +0 -2
- package/dist/cli__commands__migration-list.d.mts +0 -39
- package/dist/cli__commands__migration-list.d.mts.map +0 -1
- package/dist/cli__commands__migration-list.mjs +0 -2
- package/dist/cli__commands__migration-log.d.mts +0 -20
- package/dist/cli__commands__migration-log.d.mts.map +0 -1
- package/dist/cli__commands__migration-log.mjs +0 -2
- package/dist/cli__commands__migration-new.d.mts +0 -7
- package/dist/cli__commands__migration-new.d.mts.map +0 -1
- package/dist/cli__commands__migration-new.mjs +0 -2
- package/dist/cli__commands__migration-plan.d.mts +0 -34
- package/dist/cli__commands__migration-plan.d.mts.map +0 -1
- package/dist/cli__commands__migration-plan.mjs +0 -2
- package/dist/cli__commands__migration-show.d.mts +0 -27
- package/dist/cli__commands__migration-show.d.mts.map +0 -1
- package/dist/cli__commands__migration-show.mjs +0 -2
- package/dist/cli__commands__migration-status.d.mts +0 -55
- package/dist/cli__commands__migration-status.d.mts.map +0 -1
- package/dist/cli__commands__migration-status.mjs +0 -3
- package/dist/cli__commands__ref.d.mts +0 -7
- package/dist/cli__commands__ref.d.mts.map +0 -1
- package/dist/cli__commands__ref.mjs +0 -2
- package/dist/cli__commands__telemetry.d.mts +0 -7
- package/dist/cli__commands__telemetry.d.mts.map +0 -1
- package/dist/cli__commands__telemetry.mjs +0 -2
- package/dist/client-C6Qp5ujC-Dxb7r79B.mjs +0 -1653
- package/dist/client-C6Qp5ujC-Dxb7r79B.mjs.map +0 -1
- package/dist/code-templates-C-jirr19-DDQB0WGo.mjs +0 -295
- package/dist/code-templates-C-jirr19-DDQB0WGo.mjs.map +0 -1
- package/dist/combine-verify-results-B87B_pMc-D4l22OqY.mjs +0 -65
- package/dist/combine-verify-results-B87B_pMc-D4l22OqY.mjs.map +0 -1
- package/dist/command-helpers-BCgwcqTe-lNCtppzM.mjs +0 -776
- package/dist/command-helpers-BCgwcqTe-lNCtppzM.mjs.map +0 -1
- package/dist/contract-at-errors-BSoDI0Hw-BXeUNE3s.mjs +0 -44
- package/dist/contract-at-errors-BSoDI0Hw-BXeUNE3s.mjs.map +0 -1
- package/dist/contract-emit-BG7KL4zk-_a0J310Z.mjs +0 -331
- package/dist/contract-emit-BG7KL4zk-_a0J310Z.mjs.map +0 -1
- package/dist/contract-emit-BkEj3siG-C8M-IyvS.mjs +0 -2
- package/dist/contract-emit-CvwNE4R1-DEn1V1Zo.d.mts +0 -7
- package/dist/contract-emit-CvwNE4R1-DEn1V1Zo.d.mts.map +0 -1
- package/dist/contract-emit-GknCmgn_-BSWdFprC.mjs +0 -154
- package/dist/contract-emit-GknCmgn_-BSWdFprC.mjs.map +0 -1
- package/dist/contract-enrichment-DrMvvY2A-DxNGeF4d.mjs +0 -56
- package/dist/contract-enrichment-DrMvvY2A-DxNGeF4d.mjs.map +0 -1
- package/dist/contract-infer-BwIDGl7v-CZrdWXAr.mjs +0 -74
- package/dist/contract-infer-BwIDGl7v-CZrdWXAr.mjs.map +0 -1
- package/dist/contract-snapshot-resolution-De2VvOJn-DqIAV9cr.mjs +0 -82
- package/dist/contract-snapshot-resolution-De2VvOJn-DqIAV9cr.mjs.map +0 -1
- package/dist/contract-space-aggregate-loader-fa04OIaL-B7LXHle3.mjs +0 -303
- package/dist/contract-space-aggregate-loader-fa04OIaL-B7LXHle3.mjs.map +0 -1
- package/dist/db-init-BolXrKAI-CwG-zEDW.mjs +0 -109
- package/dist/db-init-BolXrKAI-CwG-zEDW.mjs.map +0 -1
- package/dist/db-init-failure-B5cQML4A-5zr-99Ek.mjs +0 -44
- package/dist/db-init-failure-B5cQML4A-5zr-99Ek.mjs.map +0 -1
- package/dist/db-schema-DvdLBdEY-Dl6e1d_Y.mjs +0 -48
- package/dist/db-schema-DvdLBdEY-Dl6e1d_Y.mjs.map +0 -1
- package/dist/db-sign-DCp4c8CA-B3tun-kh.mjs +0 -172
- package/dist/db-sign-DCp4c8CA-B3tun-kh.mjs.map +0 -1
- package/dist/db-update-CfayE8WE-CPhQyDaZ.mjs +0 -171
- package/dist/db-update-CfayE8WE-CPhQyDaZ.mjs.map +0 -1
- package/dist/db-verify-BN7IrOAL-kEzRNJUl.mjs +0 -358
- package/dist/db-verify-BN7IrOAL-kEzRNJUl.mjs.map +0 -1
- package/dist/exports-B3xY_FHp.mjs.map +0 -1
- package/dist/exports-BZLNbtkY.mjs.map +0 -1
- package/dist/exports-CqNUihkI.mjs +0 -276
- package/dist/exports-CqNUihkI.mjs.map +0 -1
- package/dist/format-Ca7dUE6B-BMFqrc6D.mjs +0 -110
- package/dist/format-Ca7dUE6B-BMFqrc6D.mjs.map +0 -1
- package/dist/framework-components-Bau7o1-J-DTp6AUoq.mjs +0 -60
- package/dist/framework-components-Bau7o1-J-DTp6AUoq.mjs.map +0 -1
- package/dist/global-flags-BpoOYtNZ-CHZD3fud.d.mts +0 -35
- package/dist/global-flags-BpoOYtNZ-CHZD3fud.d.mts.map +0 -1
- package/dist/index-mCsi_ND5.d.mts.map +0 -1
- package/dist/init-DC7zPKka-BSiUrPw3.mjs +0 -800
- package/dist/init-DC7zPKka-BSiUrPw3.mjs.map +0 -1
- package/dist/inspect-live-schema-BPHXdvqW-DrzL2ZkQ.mjs +0 -95
- package/dist/inspect-live-schema-BPHXdvqW-DrzL2ZkQ.mjs.map +0 -1
- package/dist/integrity-violation-to-check-failure-BmWMLAae-DqiC4_3p.mjs +0 -115
- package/dist/integrity-violation-to-check-failure-BmWMLAae-DqiC4_3p.mjs.map +0 -1
- package/dist/invariants-Bltd8DyA-Cg_1Lu0q.mjs +0 -53
- package/dist/invariants-Bltd8DyA-Cg_1Lu0q.mjs.map +0 -1
- package/dist/legend-DGv9e2_0-BtetgRz4.mjs +0 -22
- package/dist/legend-DGv9e2_0-BtetgRz4.mjs.map +0 -1
- package/dist/migrate-BXATUjYE-Do79Imf-.mjs +0 -343
- package/dist/migrate-BXATUjYE-Do79Imf-.mjs.map +0 -1
- package/dist/migrate-failure-D4NvkoCk-0bttmQs2.mjs +0 -96
- package/dist/migrate-failure-D4NvkoCk-0bttmQs2.mjs.map +0 -1
- package/dist/migrate-show-B8UJ9dmb-C4RTPbDk.d.mts +0 -63
- package/dist/migrate-show-B8UJ9dmb-C4RTPbDk.d.mts.map +0 -1
- package/dist/migrate-show-C_9xR4Qt-B-Y0PGKw.mjs +0 -199
- package/dist/migrate-show-C_9xR4Qt-B-Y0PGKw.mjs.map +0 -1
- package/dist/migration-check-Bbdw7t-r-C2VTMK61.mjs +0 -358
- package/dist/migration-check-Bbdw7t-r-C2VTMK61.mjs.map +0 -1
- package/dist/migration-check-DbHgYK7F-7mtYNVFu.mjs +0 -163
- package/dist/migration-check-DbHgYK7F-7mtYNVFu.mjs.map +0 -1
- package/dist/migration-command-scaffold-SzjR2naH-Yi3-rNfX.mjs +0 -121
- package/dist/migration-command-scaffold-SzjR2naH-Yi3-rNfX.mjs.map +0 -1
- package/dist/migration-graph-B8WxW_tm-qGNf7jQh.mjs +0 -35
- package/dist/migration-graph-B8WxW_tm-qGNf7jQh.mjs.map +0 -1
- package/dist/migration-graph-BOwu76mF-kOTWFvfV.mjs +0 -144
- package/dist/migration-graph-BOwu76mF-kOTWFvfV.mjs.map +0 -1
- package/dist/migration-graph-command-render-h0VdTkSM-C3rPZEw1.mjs +0 -2127
- package/dist/migration-graph-command-render-h0VdTkSM-C3rPZEw1.mjs.map +0 -1
- package/dist/migration-graph-sections-BBHPOFN--COKyUKQ4.mjs +0 -69
- package/dist/migration-graph-sections-BBHPOFN--COKyUKQ4.mjs.map +0 -1
- package/dist/migration-list-CKuYcMQq-CubZzE9i.mjs +0 -130
- package/dist/migration-list-CKuYcMQq-CubZzE9i.mjs.map +0 -1
- package/dist/migration-list-CY08bD8I-PoXuv94c.mjs +0 -102
- package/dist/migration-list-CY08bD8I-PoXuv94c.mjs.map +0 -1
- package/dist/migration-log-CUbGla-9-BLIrWjwN.mjs +0 -123
- package/dist/migration-log-CUbGla-9-BLIrWjwN.mjs.map +0 -1
- package/dist/migration-log-table-eES9lPjv-BCFm8qJr.mjs +0 -116
- package/dist/migration-log-table-eES9lPjv-BCFm8qJr.mjs.map +0 -1
- package/dist/migration-new-ChvZjf5X-BFhEM0BF.mjs +0 -70
- package/dist/migration-new-ChvZjf5X-BFhEM0BF.mjs.map +0 -1
- package/dist/migration-new-DXMvJdbB-BkrpDfcr.mjs +0 -130
- package/dist/migration-new-DXMvJdbB-BkrpDfcr.mjs.map +0 -1
- package/dist/migration-path-target-JDWqF9kj-B7zoMq6X.mjs +0 -38
- package/dist/migration-path-target-JDWqF9kj-B7zoMq6X.mjs.map +0 -1
- package/dist/migration-plan-BV9f5Xjq-DiyscOMc.d.mts +0 -164
- package/dist/migration-plan-BV9f5Xjq-DiyscOMc.d.mts.map +0 -1
- package/dist/migration-plan-Bz1hOTHF-CFCtPuOv.mjs +0 -192
- package/dist/migration-plan-Bz1hOTHF-CFCtPuOv.mjs.map +0 -1
- package/dist/migration-plan-Cfz0efEL-DMNFq5b2.mjs +0 -621
- package/dist/migration-plan-Cfz0efEL-DMNFq5b2.mjs.map +0 -1
- package/dist/migration-show-BGdR1xnT-Cg3M0FPh.mjs +0 -178
- package/dist/migration-show-BGdR1xnT-Cg3M0FPh.mjs.map +0 -1
- package/dist/migration-status-BIrAJ5af-D-_mMBUA.mjs +0 -427
- package/dist/migration-status-BIrAJ5af-D-_mMBUA.mjs.map +0 -1
- package/dist/output-DP3woAjV-BxCr9nHA.mjs.map +0 -1
- package/dist/progress-adapter-CjAeTxY_-BXYSd1eY.mjs +0 -43
- package/dist/progress-adapter-CjAeTxY_-BXYSd1eY.mjs.map +0 -1
- package/dist/project-import-root-B4nM-VWX-D__g0hTb.mjs +0 -101
- package/dist/project-import-root-B4nM-VWX-D__g0hTb.mjs.map +0 -1
- package/dist/redact-secrets-DuKoiMax-Cfe2XVhc.mjs +0 -1476
- package/dist/redact-secrets-DuKoiMax-Cfe2XVhc.mjs.map +0 -1
- package/dist/ref-4x6BGSsd-ClaoZIQh.mjs +0 -95
- package/dist/ref-4x6BGSsd-ClaoZIQh.mjs.map +0 -1
- package/dist/ref-WXLIR9Xm-12IUtoBU.mjs +0 -104
- package/dist/ref-WXLIR9Xm-12IUtoBU.mjs.map +0 -1
- package/dist/ref-advancement-DaZBzm7m-WoqnFAyr.mjs +0 -96
- package/dist/ref-advancement-DaZBzm7m-WoqnFAyr.mjs.map +0 -1
- package/dist/ref-resolution-BSN1amKm-C6-TTM2N.mjs +0 -16
- package/dist/ref-resolution-BSN1amKm-C6-TTM2N.mjs.map +0 -1
- package/dist/refs-fQLFcMoq-aTBeUU9H.mjs +0 -47
- package/dist/refs-fQLFcMoq-aTBeUU9H.mjs.map +0 -1
- package/dist/schemas-BGgjiKV0-Rh8RlMCl.d.mts +0 -206
- package/dist/schemas-BGgjiKV0-Rh8RlMCl.d.mts.map +0 -1
- package/dist/schemas-CYSD83Cu-Bff4PPoi.mjs +0 -119
- package/dist/schemas-CYSD83Cu-Bff4PPoi.mjs.map +0 -1
- package/dist/shells-BenrJHp2.mjs.map +0 -1
- package/dist/shells-COHV_Ozf.d.mts.map +0 -1
- package/dist/telemetry-BD52LETE-Cqeb7ebh.mjs +0 -121
- package/dist/telemetry-BD52LETE-Cqeb7ebh.mjs.map +0 -1
- package/dist/terminal-ui-BN4QOsm5-Bf3pim46.d.mts +0 -133
- package/dist/terminal-ui-BN4QOsm5-Bf3pim46.d.mts.map +0 -1
- package/dist/terminal-ui-Cqz4LVL7-B0mcVdLy.mjs +0 -676
- package/dist/terminal-ui-Cqz4LVL7-B0mcVdLy.mjs.map +0 -1
- package/dist/verify-Cogr_Fj9-D3lCWA6t.mjs +0 -283
- package/dist/verify-Cogr_Fj9-D3lCWA6t.mjs.map +0 -1
|
@@ -0,0 +1,4686 @@
|
|
|
1
|
+
import { r as getEmittedArtifactPaths, t as emit } from "./exports-BCLg4CiV-05kuwcb0.mjs";
|
|
2
|
+
import { O as errorNoInvariantPath, P as errorUnknownInvariant, b as errorInvalidRefName, i as errorContractDeserializationFailed, t as MigrationToolsError } from "./errors-CXbGUWGu-CstQHPh6.mjs";
|
|
3
|
+
import { a as snapshotsImportPathFrom, o as writeContractSnapshot, r as readContractSnapshotJson, t as contractSnapshotDir } from "./contract-snapshot-store-B0G1Fw_F-CHc4QfKb.mjs";
|
|
4
|
+
import { n as verifyMigrationHash, t as computeMigrationHash } from "./hash-DH8hl5Rq-BpM2t3Nu.mjs";
|
|
5
|
+
import { t as deriveProvidedInvariants } from "./invariants-CTHy88OP-CYI2a8IO.mjs";
|
|
6
|
+
import { i as materialiseExtensionMigrationPackageIfMissing, r as formatMigrationDirName, u as writeMigrationPackage } from "./io-635GlNx1-D3zrw2qu.mjs";
|
|
7
|
+
import { t as EMPTY_CONTRACT_HASH } from "./constants-CM-w38DQ-Cz_EXbOc.mjs";
|
|
8
|
+
import { n as isGraphNode, t as assertHashIsGraphNode } from "./graph-membership-BViWUMpP-C4lYYJdU.mjs";
|
|
9
|
+
import { a as findPath, o as findPathWithDecision, r as findLatestMigration } from "./migration-graph-_SJImD5V-BURyY1KM.mjs";
|
|
10
|
+
import { d as writeRef, i as readRefs, l as validateRefName, n as deleteRef, o as refsByContractHash, t as HEAD_REF_NAME, u as validateRefValue } from "./refs-WK86uYYp-gM8tvWQP.mjs";
|
|
11
|
+
import { a as isValidSpaceId, c as spaceMigrationDirectory, l as spaceRefsDirectory, o as listContractSpaceDirectories, s as readContractSpaceHeadRef, t as APP_SPACE_ID$1 } from "./verify-contract-spaces-DhJTb0zM-B62maCfE.mjs";
|
|
12
|
+
import { d as resolveRecordedPath, f as verifyMigration, l as planMigration, n as buildFabricatedMigrationEdge, r as collectAggregateNamespaces, s as loadContractSpaceAggregate, t as allStorageElementsExternal, u as requireHeadRef } from "./aggregate-CPZB020V.mjs";
|
|
13
|
+
import { i as emitContractSpaceArtifacts, o as planAllSpaces } from "./spaces-4qMOAYE9.mjs";
|
|
14
|
+
import { c as internalImportRoot, o as importRootForDependencies, r as createImportSpecifierResolver } from "./import-roots-CNFBPAT1.mjs";
|
|
15
|
+
import { n as parseContractRef, r as parseMigrationRef } from "./ref-resolution-HeNBXW53.mjs";
|
|
16
|
+
import { i as writeMigrationTs } from "./migration-ts-DdVm0rws.mjs";
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import { ifDefined } from "@prisma/orm-framework/utils/defined";
|
|
19
|
+
import { structuredError } from "@prisma/orm-framework/utils/structured-error";
|
|
20
|
+
import { blindCast, castAs } from "@prisma/orm-framework/utils/casts";
|
|
21
|
+
import { InternalError } from "@prisma/orm-framework/utils/internal-error";
|
|
22
|
+
import { CliStructuredError, CliStructuredError as CliStructuredError$1, errorConfigValidation as errorConfigValidation$1, errorContractConfigMissing, errorContractValidationFailed, errorDatabaseConnectionRequired, errorDriverRequired, errorFileNotFound, errorMigrationPlanningFailed, errorTargetMigrationNotSupported, errorUnexpected } from "@prisma/orm-framework/errors/control";
|
|
23
|
+
import { APP_SPACE_ID, createControlStack, hasMigrations, hasOperationPreview, hasPslContractInfer, hasSchemaSubjectClassifier, hasSchemaView } from "@prisma/orm-framework/components/control";
|
|
24
|
+
import { notOk, ok } from "@prisma/orm-framework/utils/result";
|
|
25
|
+
import { mergeCapabilityMatrices } from "@prisma/orm-framework/components/components";
|
|
26
|
+
import { errorConsentPlanMismatch, errorDestructiveChanges, errorHashMismatch, errorMarkerMissing, errorRunnerFailed, errorRuntime, errorTargetMismatch } from "@prisma/orm-framework/errors/execution";
|
|
27
|
+
import "@prisma/orm-framework/errors/migration";
|
|
28
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "pathe";
|
|
29
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
30
|
+
import { createHash } from "node:crypto";
|
|
31
|
+
import { canonicalizeJson } from "@prisma/orm-framework/components/utils";
|
|
32
|
+
import { abortable } from "@prisma/orm-framework/utils/abortable";
|
|
33
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
34
|
+
//#region ../../../1-framework/3-tooling/cli/dist/ref-h6InR6dO.mjs
|
|
35
|
+
/**
|
|
36
|
+
* The typed remediation the CLI attaches to its own errors and findings.
|
|
37
|
+
* Constructors live here rather than in any library the CLI calls: naming a
|
|
38
|
+
* runnable invocation is something only the CLI can do.
|
|
39
|
+
*/
|
|
40
|
+
function runCommandAction(label, command) {
|
|
41
|
+
return {
|
|
42
|
+
kind: "run-command",
|
|
43
|
+
label,
|
|
44
|
+
command
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Advice the user acts on themselves — there is no command to run. */
|
|
48
|
+
function chooseAction(label) {
|
|
49
|
+
return {
|
|
50
|
+
kind: "user-choice",
|
|
51
|
+
label
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A CLI-raised error that carries the typed remediation as well as the prose.
|
|
56
|
+
* Only the CLI knows which invocation fixes a failure, so only errors raised
|
|
57
|
+
* here spell one; the foundation classes every library also raises keep
|
|
58
|
+
* carrying `code`, `why` and `fix` alone.
|
|
59
|
+
*
|
|
60
|
+
* Both fields are required: the `fix` prose is what the commander shell still
|
|
61
|
+
* renders, and the handler boundary drops it in favour of `nextActions` when it
|
|
62
|
+
* settles the engine's envelope.
|
|
63
|
+
*/
|
|
64
|
+
var ActionableCliError = class extends CliStructuredError$1 {
|
|
65
|
+
nextActions;
|
|
66
|
+
constructor(code, summary, options) {
|
|
67
|
+
super(code, summary, options);
|
|
68
|
+
this.nextActions = options.nextActions;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* A command that accepts a contract reference both as a positional and as a
|
|
73
|
+
* flag was given both. Structured rather than a bare stderr line, so a script
|
|
74
|
+
* sees the same envelope every other invocation failure produces.
|
|
75
|
+
*/
|
|
76
|
+
function errorContractArgConflict(options) {
|
|
77
|
+
const fix = "Pass the contract reference once — either as the positional argument or as --contract.";
|
|
78
|
+
return new ActionableCliError("CLI.CONTRACT_ARG_CONFLICT", "Cannot specify both a positional contract argument and --contract", {
|
|
79
|
+
why: `The positional argument names "${options.positional}" and --contract names "${options.flag}"; there is no rule for which wins.`,
|
|
80
|
+
fix,
|
|
81
|
+
nextActions: [chooseAction(fix)],
|
|
82
|
+
meta: {
|
|
83
|
+
positional: options.positional,
|
|
84
|
+
flag: options.flag
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function errorRefSetHashNotInGraph(resolvedHash, reachableHashes, graphTipHash) {
|
|
89
|
+
const reachableList = reachableHashes.length > 0 ? reachableHashes.join(", ") : "(none — migration graph is empty)";
|
|
90
|
+
const fix = reachableHashes.length > 0 ? graphTipHash !== null ? `Set the ref to a graph-node hash such as ${graphTipHash}, or run \`prisma-cli migration plan\` to extend the graph.` : "Set the ref to a hash that appears in the migration graph." : "Run `prisma-cli migration plan` first.";
|
|
91
|
+
const nextActions = reachableHashes.length > 0 ? graphTipHash !== null ? [chooseAction(`Set the ref to a graph-node hash such as ${graphTipHash}`), runCommandAction("Extend the migration graph", "prisma-cli migration plan")] : [chooseAction("Set the ref to a hash that appears in the migration graph")] : [runCommandAction("Plan the first migration", "prisma-cli migration plan")];
|
|
92
|
+
return new ActionableCliError("MIGRATION.HASH_NOT_IN_GRAPH", `Resolved contract hash is not in the migration graph: ${resolvedHash}`, {
|
|
93
|
+
why: reachableHashes.length > 0 ? `The migration graph reaches ${reachableList}; resolved ${resolvedHash} isn't a graph node.` : "The migration graph is empty — no hashes reachable.",
|
|
94
|
+
fix,
|
|
95
|
+
nextActions,
|
|
96
|
+
meta: {
|
|
97
|
+
resolvedHash,
|
|
98
|
+
reachableHashes: [...reachableHashes],
|
|
99
|
+
...graphTipHash !== null ? { graphTipHash } : {}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function errorRefSetEmptySentinel(hash) {
|
|
104
|
+
return new ActionableCliError("MIGRATION.REF_SET_EMPTY_SENTINEL", `Cannot set ref to the empty-database sentinel: ${hash}`, {
|
|
105
|
+
why: "The empty-database sentinel is a planner internal; it is not a valid ref target.",
|
|
106
|
+
fix: "Set the ref to a contract hash from the migration graph, or use another ref name.",
|
|
107
|
+
nextActions: [chooseAction("Set the ref to a contract hash from the migration graph"), chooseAction("Or use another ref name")],
|
|
108
|
+
meta: { hash }
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* `--legend` was combined with a machine-readable or silent output flag.
|
|
113
|
+
* The legend is human-only decoration on stderr.
|
|
114
|
+
*/
|
|
115
|
+
function errorLegendHumanOnly(conflictingFlag) {
|
|
116
|
+
return new ActionableCliError("MIGRATION.LEGEND_HUMAN_ONLY", "`--legend` is only available for human-readable output", {
|
|
117
|
+
why: `\`--legend\` prints a glyph key to stderr and cannot be combined with ${conflictingFlag}.`,
|
|
118
|
+
fix: `Omit ${conflictingFlag} to print the legend alongside the tree, or omit --legend when using ${conflictingFlag}.`,
|
|
119
|
+
nextActions: [chooseAction(`Omit ${conflictingFlag} to print the legend alongside the tree`), chooseAction(`Or omit --legend when using ${conflictingFlag}`)],
|
|
120
|
+
meta: { conflictingFlag }
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* `--space <id>` was given a value that doesn't satisfy the contract-space
|
|
125
|
+
* naming rule (`[a-z][a-z0-9_-]{0,63}` per `isValidSpaceId`). Fires before
|
|
126
|
+
* any fs work — the input is syntactically rejected the same way an on-disk
|
|
127
|
+
* directory with that name would be skipped by the enumerator.
|
|
128
|
+
*/
|
|
129
|
+
function errorInvalidSpaceId(spaceId) {
|
|
130
|
+
return new ActionableCliError("MIGRATION.INVALID_SPACE_ID", `Invalid contract space id: ${spaceId}`, {
|
|
131
|
+
why: "Contract space ids must match [a-z][a-z0-9_-]{0,63} (lowercase, starts with a letter, max 64 characters — the rule applied to every on-disk space directory).",
|
|
132
|
+
fix: "Pass a space id that matches the directory naming rule, or omit --space to list every space.",
|
|
133
|
+
nextActions: [chooseAction("Pass a space id matching [a-z][a-z0-9_-]{0,63}"), runCommandAction("Or list every space", "prisma-cli migration list")],
|
|
134
|
+
meta: { spaceId }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* `migration list --space <id>` was given a contract-space id that has no
|
|
139
|
+
* directory under `migrations/`. Distinct from "the space exists but is
|
|
140
|
+
* empty" — that path renders the empty-state line and exits 0 per the
|
|
141
|
+
* slice spec § Empty-state. This error fires only when `<projectMigrationsDir>/<spaceId>`
|
|
142
|
+
* does not exist on disk.
|
|
143
|
+
*
|
|
144
|
+
* `availableSpaces` lists the contract-space directory names actually
|
|
145
|
+
* present, sorted lex-asc, so the diagnostic can suggest a near match
|
|
146
|
+
* without making the user reach for `ls`.
|
|
147
|
+
*/
|
|
148
|
+
function errorSpaceNotFound(spaceId, availableSpaces) {
|
|
149
|
+
const availableList = availableSpaces.length > 0 ? availableSpaces.join(", ") : "(none — no contract spaces on disk yet)";
|
|
150
|
+
const fix = availableSpaces.length > 0 ? `Pick one of: ${availableList}. Run \`prisma-cli migration list\` (no --space) to see every space's migrations.` : "Author a migration with `prisma-cli migration new` to create the first contract-space directory.";
|
|
151
|
+
const nextActions = availableSpaces.length > 0 ? [chooseAction(`Pick one of: ${availableList}`), runCommandAction("See every space's migrations", "prisma-cli migration list")] : [runCommandAction("Author the first migration, which creates the contract-space directory", "prisma-cli migration new")];
|
|
152
|
+
return new ActionableCliError("MIGRATION.SPACE_NOT_FOUND", `Unknown contract space: ${spaceId}`, {
|
|
153
|
+
why: `No directory named "${spaceId}" exists under the migrations root.`,
|
|
154
|
+
fix,
|
|
155
|
+
nextActions,
|
|
156
|
+
meta: {
|
|
157
|
+
spaceId,
|
|
158
|
+
availableSpaces: [...availableSpaces]
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A `migration show` target resolved to a directory or a graph node, but no
|
|
164
|
+
* on-disk package was loaded for it.
|
|
165
|
+
*/
|
|
166
|
+
function errorMigrationPackageNotFound(why) {
|
|
167
|
+
return new ActionableCliError("MIGRATION.PACKAGE_NOT_FOUND", "Migration package not found", {
|
|
168
|
+
why,
|
|
169
|
+
fix: "Pass a directory name, hash prefix, or path to an on-disk app-space migration package.",
|
|
170
|
+
nextActions: [chooseAction("Pass a directory name, hash prefix, or path to an on-disk app-space migration package"), runCommandAction("List what is on disk", "prisma-cli migration list")]
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** The app space has no migration packages at all, so no target can resolve. */
|
|
174
|
+
function errorNoMigrations(appMigrationsRelative) {
|
|
175
|
+
return new ActionableCliError("MIGRATION.NO_MIGRATIONS", "No migrations found", {
|
|
176
|
+
why: `No migration packages found in ${appMigrationsRelative}`,
|
|
177
|
+
fix: "Run `prisma-cli migration plan` to create a migration first.",
|
|
178
|
+
nextActions: [runCommandAction("Create the first migration", "prisma-cli migration plan")]
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function errorRefSetBundleNotFound(hash) {
|
|
182
|
+
return new ActionableCliError("MIGRATION.REF_SET_BUNDLE_NOT_FOUND", `No migration bundle matches graph-node hash ${hash}`, {
|
|
183
|
+
why: `The hash is a graph node but no on-disk bundle has metadata.to = ${hash}.`,
|
|
184
|
+
fix: "Run `pnpm fixtures:check`, or re-emit the migration that produces this hash so its bundle is restored.",
|
|
185
|
+
nextActions: [runCommandAction("Restore the checked-in fixtures", "pnpm fixtures:check"), chooseAction("Or re-emit the migration that produces this hash")],
|
|
186
|
+
meta: { hash }
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function errorPlanForgotTheFlag(resolvedHash, reachableRefs, graphTipHash, options) {
|
|
190
|
+
const reachableList = reachableRefs.length > 0 ? reachableRefs.map((r) => `${r.name} (${r.hash})`).join(", ") : "(none)";
|
|
191
|
+
const refFix = reachableRefs.length > 0 ? `Run migration plan with ${reachableRefs.map((r) => `--from ${r.name}`).join(" or ")}.` : graphTipHash !== null ? `Run migration plan --from ${graphTipHash}.` : "Commit pending migrations first, then run migration plan.";
|
|
192
|
+
const nextActions = reachableRefs.length > 0 ? reachableRefs.map((ref) => runCommandAction(`Plan from ${ref.name}`, `prisma-cli migration plan --from ${ref.name}`)) : graphTipHash !== null ? [runCommandAction("Plan from the graph tip", `prisma-cli migration plan --from ${graphTipHash}`)] : [chooseAction("Commit pending migrations first, then run migration plan")];
|
|
193
|
+
return new ActionableCliError("MIGRATION.HASH_NOT_IN_GRAPH", `Resolved from-hash is not in the migration graph: ${resolvedHash}`, {
|
|
194
|
+
why: `The migration graph reaches ${reachableList}; resolved ${resolvedHash} isn't a graph node.`,
|
|
195
|
+
fix: refFix,
|
|
196
|
+
nextActions,
|
|
197
|
+
meta: {
|
|
198
|
+
resolvedHash,
|
|
199
|
+
reachableRefs: reachableRefs.map((r) => r.name),
|
|
200
|
+
...graphTipHash !== null ? { graphTipHash } : {}
|
|
201
|
+
},
|
|
202
|
+
...ifDefined("cause", options?.cause)
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* `viaRef: true` (the default) mirrors migration-tools' `errorRefNotResolvable`:
|
|
207
|
+
* a ref name with no pointer file, where the fallback hash isn't a graph
|
|
208
|
+
* node either — there's nothing to materialize a contract from.
|
|
209
|
+
* `viaRef: false` is a distinct, ref-independent case: an explicit `--from
|
|
210
|
+
* <hash>` that doesn't name a ref, on an empty migration graph, so there is
|
|
211
|
+
* no graph node and no ref to resolve a contract through.
|
|
212
|
+
*/
|
|
213
|
+
function errorSnapshotMissing(identifier, options) {
|
|
214
|
+
const viaRef = options?.viaRef !== false;
|
|
215
|
+
const fix = viaRef ? `Create the ref with "prisma-cli ref set ${identifier} <hash>" (or advance it via "prisma-cli db update --advance-ref ${identifier}"), or pass a hash that is a node in the migration graph.` : `No contract source exists for hash "${identifier}" on an empty migration graph. Use --from with a ref name (its contract resolves through the snapshot store), or run db update first.`;
|
|
216
|
+
const nextActions = viaRef ? [
|
|
217
|
+
runCommandAction(`Create the ref "${identifier}"`, `prisma-cli ref set ${identifier} <hash>`),
|
|
218
|
+
runCommandAction("Or advance it from the database", `prisma-cli db update --advance-ref ${identifier}`),
|
|
219
|
+
chooseAction("Or pass a hash that is a node in the migration graph")
|
|
220
|
+
] : [chooseAction("Pass --from a ref name, whose contract resolves through the snapshot store"), runCommandAction("Or populate the graph first", "prisma-cli db update")];
|
|
221
|
+
return new ActionableCliError("MIGRATION.SNAPSHOT_MISSING", viaRef ? `Ref "${identifier}" is not resolvable` : `No contract source for from-hash "${identifier}"`, {
|
|
222
|
+
why: viaRef ? `Ref "${identifier}" has no pointer file, and the hash being resolved is not a node in the migration graph either.` : `Hash "${identifier}" is not a node in the migration graph (the graph is empty), and it does not name a ref either.`,
|
|
223
|
+
fix,
|
|
224
|
+
nextActions,
|
|
225
|
+
meta: {
|
|
226
|
+
identifier,
|
|
227
|
+
viaRef
|
|
228
|
+
},
|
|
229
|
+
...ifDefined("cause", options?.cause)
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function errorMarkerMismatch(markerHash, reachableHashes, graphTip) {
|
|
233
|
+
const reachableList = reachableHashes.length > 0 ? reachableHashes.join(", ") : "(none — migration graph is empty)";
|
|
234
|
+
const planFromFix = graphTip !== null ? `Run \`prisma-cli migration plan --from ${graphTip}\` if the live marker is canonical and the on-disk graph needs catching up.` : "Run `prisma-cli migration plan` if the live marker is canonical and the on-disk graph needs catching up.";
|
|
235
|
+
const planCommand = graphTip !== null ? `prisma-cli migration plan --from ${graphTip}` : "prisma-cli migration plan";
|
|
236
|
+
return new ActionableCliError("MIGRATION.MARKER_MISMATCH", "Database marker is not reachable in the on-disk migration graph", {
|
|
237
|
+
why: `DB marker is ${markerHash}, but the on-disk migration graph reaches: ${reachableList}.`,
|
|
238
|
+
fix: [
|
|
239
|
+
planFromFix,
|
|
240
|
+
`Run \`prisma-cli ref set db ${markerHash}\` if the on-disk graph is canonical and the local \`db\` ref drifted.`,
|
|
241
|
+
"Investigate whether the database was migrated by an out-of-band process."
|
|
242
|
+
].join("\n"),
|
|
243
|
+
nextActions: [
|
|
244
|
+
runCommandAction("Catch the on-disk graph up to the live marker", planCommand),
|
|
245
|
+
runCommandAction("Point the local db ref at the live marker", `prisma-cli ref set db ${markerHash}`),
|
|
246
|
+
chooseAction("Investigate whether the database was migrated by an out-of-band process")
|
|
247
|
+
],
|
|
248
|
+
meta: {
|
|
249
|
+
markerHash,
|
|
250
|
+
reachableHashes: [...reachableHashes],
|
|
251
|
+
...graphTip !== null ? { graphTip } : {}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const ROLLBACK_IS_DESTRUCTIVE = "A rollback (reverse) plan is expected to contain destructive (DROP) operations — review them before applying";
|
|
256
|
+
const NARROWER_CASES_NEED_A_HINT = "Narrower cases (rename inference, re-adding a required field without a safe default, or a type change that needs data) may additionally need a hint in the planned migration";
|
|
257
|
+
function errorPathUnreachable(failure) {
|
|
258
|
+
const meta = failure.meta ?? {};
|
|
259
|
+
const fromHashMeta = typeof meta["fromHash"] === "string" ? meta["fromHash"] : null;
|
|
260
|
+
const planFromHash = fromHashMeta === "<empty>" ? null : fromHashMeta;
|
|
261
|
+
const targetHash = typeof meta["targetHash"] === "string" ? meta["targetHash"] : typeof meta["target"] === "string" ? meta["target"] : null;
|
|
262
|
+
const deadEnds = meta["deadEnds"];
|
|
263
|
+
const deadEndsSuffix = Array.isArray(deadEnds) && deadEnds.length > 0 ? ` Dead-ends: ${deadEnds.map(String).join(", ")}.` : "";
|
|
264
|
+
const neverPlanned = meta["kind"] === "neverPlanned";
|
|
265
|
+
const planCommand = (() => {
|
|
266
|
+
if (neverPlanned) return "prisma-cli migration plan --name <slug>";
|
|
267
|
+
if (planFromHash !== null && targetHash !== null) return `prisma-cli migration plan --from ${planFromHash} --to ${targetHash} --name <slug>`;
|
|
268
|
+
if (targetHash !== null) return `prisma-cli migration plan --to ${targetHash} --name <slug>`;
|
|
269
|
+
if (planFromHash !== null) return `prisma-cli migration plan --from ${planFromHash} --name <slug>`;
|
|
270
|
+
return "prisma-cli migration plan";
|
|
271
|
+
})();
|
|
272
|
+
const applyCommand = targetHash !== null && !neverPlanned ? `prisma-cli migrate --to ${targetHash}` : "prisma-cli migrate";
|
|
273
|
+
return new ActionableCliError("MIGRATION.PATH_UNREACHABLE", failure.summary, {
|
|
274
|
+
why: failure.why ?? `Cannot reach target "${targetHash ?? "<unknown>"}" from current marker "${fromHashMeta ?? "<unknown>"}".${deadEndsSuffix}`,
|
|
275
|
+
fix: [
|
|
276
|
+
"Plan the missing edge, then apply it:",
|
|
277
|
+
` 1. ${planCommand}`,
|
|
278
|
+
` 2. ${applyCommand}`,
|
|
279
|
+
`${ROLLBACK_IS_DESTRUCTIVE}.`,
|
|
280
|
+
`${NARROWER_CASES_NEED_A_HINT}.`,
|
|
281
|
+
"Inspect the on-disk graph with `prisma-cli migration list`, or `prisma-cli migration show <bundle>` for any bundle in the path you expected."
|
|
282
|
+
].join("\n"),
|
|
283
|
+
nextActions: [
|
|
284
|
+
runCommandAction("Plan the missing edge", planCommand),
|
|
285
|
+
runCommandAction("Apply it", applyCommand),
|
|
286
|
+
chooseAction(ROLLBACK_IS_DESTRUCTIVE),
|
|
287
|
+
chooseAction(NARROWER_CASES_NEED_A_HINT),
|
|
288
|
+
runCommandAction("Inspect the on-disk graph", "prisma-cli migration list"),
|
|
289
|
+
runCommandAction("Inspect a bundle in the path you expected", "prisma-cli migration show <bundle>")
|
|
290
|
+
],
|
|
291
|
+
meta: { ...meta }
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Shared "needs a live database" precondition for read verbs that consult the
|
|
296
|
+
* marker/ledger (`migration log`, `migration status`). A command needs both a
|
|
297
|
+
* connection string and a control-plane driver; either missing yields the same
|
|
298
|
+
* `CONFIG.DB_CONNECTION_REQUIRED` envelope with `meta.missingFlags` (canonical long-form flags
|
|
299
|
+
* per CLI Style Guide §Errors) so callers can react programmatically. Returns
|
|
300
|
+
* `null` when both are present.
|
|
301
|
+
*/
|
|
302
|
+
function requireLiveDatabase(args) {
|
|
303
|
+
if (args.dbConnection && args.hasDriver) return null;
|
|
304
|
+
const missingFlags = args.dbConnection ? [] : ["--db"];
|
|
305
|
+
return errorDatabaseConnectionRequired({
|
|
306
|
+
why: args.why,
|
|
307
|
+
missingFlags,
|
|
308
|
+
...ifDefined("commandName", args.commandName),
|
|
309
|
+
...ifDefined("retryCommand", args.retryCommand)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Maps a `RefResolutionError` from the contract/migration reference
|
|
314
|
+
* resolver into a CLI structured error envelope.
|
|
315
|
+
*/
|
|
316
|
+
/**
|
|
317
|
+
* A migration ref (dirName or hash-prefix) resolves in more than one contract
|
|
318
|
+
* space. The user must qualify with `--space <id>` to disambiguate.
|
|
319
|
+
*/
|
|
320
|
+
function errorAmbiguousMigrationRef(ref, spaceIds) {
|
|
321
|
+
const spaceList = spaceIds.join(", ");
|
|
322
|
+
return new ActionableCliError("MIGRATION.AMBIGUOUS_MIGRATION_REF", `Ambiguous migration reference: "${ref}" resolves in multiple spaces — qualify with --space <id>`, {
|
|
323
|
+
why: `"${ref}" matches migrations in spaces: ${spaceList}.`,
|
|
324
|
+
fix: `Qualify with --space <id> to select one space. Available matching spaces: ${spaceList}.`,
|
|
325
|
+
nextActions: [chooseAction(`Qualify with --space <id>, one of: ${spaceList}`)],
|
|
326
|
+
meta: {
|
|
327
|
+
ref,
|
|
328
|
+
spaceIds: [...spaceIds]
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function mapRefResolutionError(error) {
|
|
333
|
+
switch (error.kind) {
|
|
334
|
+
case "not-found": {
|
|
335
|
+
const fix = error.grammar === "contract" ? "Provide a valid contract hash, ref name, or migration directory name." : "Provide a valid migration directory name or migration hash.";
|
|
336
|
+
return new ActionableCliError("MIGRATION.REF_NOT_FOUND", `Not a known ${error.grammar} reference: "${error.input}"`, {
|
|
337
|
+
why: `No ${error.grammar} matching "${error.input}" exists in the migration graph or refs index.`,
|
|
338
|
+
fix,
|
|
339
|
+
nextActions: [chooseAction(fix)],
|
|
340
|
+
meta: {
|
|
341
|
+
input: error.input,
|
|
342
|
+
grammar: error.grammar
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
case "ambiguous": return new ActionableCliError("MIGRATION.REF_AMBIGUOUS", `Ambiguous ${error.grammar} reference: "${error.input}"`, {
|
|
347
|
+
why: `"${error.input}" matches multiple ${error.grammar}s: ${error.candidates.join(", ")}`,
|
|
348
|
+
fix: "Provide a longer prefix or use the full hash to disambiguate.",
|
|
349
|
+
nextActions: [chooseAction("Provide a longer prefix or use the full hash to disambiguate")],
|
|
350
|
+
meta: {
|
|
351
|
+
input: error.input,
|
|
352
|
+
candidates: error.candidates,
|
|
353
|
+
grammar: error.grammar
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
case "wrong-grammar": return new ActionableCliError("MIGRATION.REF_WRONG_GRAMMAR", error.message, {
|
|
357
|
+
why: error.message,
|
|
358
|
+
fix: error.fix,
|
|
359
|
+
nextActions: [chooseAction(error.fix)],
|
|
360
|
+
meta: {
|
|
361
|
+
input: error.input,
|
|
362
|
+
expectedGrammar: error.expectedGrammar
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
case "invalid-format": return new ActionableCliError("MIGRATION.REF_INVALID_FORMAT", `Invalid reference format: "${error.input}"`, {
|
|
366
|
+
why: error.reason,
|
|
367
|
+
fix: "Provide a valid contract hash, ref name, or migration directory name.",
|
|
368
|
+
nextActions: [chooseAction("Provide a valid contract hash, ref name, or migration directory name")],
|
|
369
|
+
meta: { input: error.input }
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Asserts that all framework components are compatible with the expected family and target.
|
|
375
|
+
*
|
|
376
|
+
* This function validates that each component in the framework components array:
|
|
377
|
+
* - Has kind 'target', 'adapter', 'extension', or 'driver'
|
|
378
|
+
* - Has familyId matching expectedFamilyId
|
|
379
|
+
* - Has targetId matching expectedTargetId
|
|
380
|
+
*
|
|
381
|
+
* This validation happens at the CLI composition boundary, before passing components
|
|
382
|
+
* to typed planner/runner instances. It fills the gap between runtime validation
|
|
383
|
+
* (via the config loader's `collectConfigIssues` diagnostics) and compile-time
|
|
384
|
+
* type enforcement.
|
|
385
|
+
*
|
|
386
|
+
* @param expectedFamilyId - The expected family ID (e.g., 'sql')
|
|
387
|
+
* @param expectedTargetId - The expected target ID (e.g., 'postgres')
|
|
388
|
+
* @param frameworkComponents - Array of framework components to validate
|
|
389
|
+
* @returns The same array typed as TargetBoundComponentDescriptor
|
|
390
|
+
* @throws CliStructuredError if any component is incompatible
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```ts
|
|
394
|
+
* const configResult = await loadConfigForSections(undefined, ['family', 'target', 'adapter', 'extensions']);
|
|
395
|
+
* const config = configResult.assertOk();
|
|
396
|
+
* const frameworkComponents = [config.target, config.adapter, ...(config.extensions ?? [])];
|
|
397
|
+
*
|
|
398
|
+
* // Validate and type-narrow components before passing to planner
|
|
399
|
+
* const typedComponents = assertFrameworkComponentsCompatible(
|
|
400
|
+
* config.family.familyId,
|
|
401
|
+
* config.target.targetId,
|
|
402
|
+
* frameworkComponents
|
|
403
|
+
* );
|
|
404
|
+
*
|
|
405
|
+
* const planner = target.migrations.createPlanner(familyInstance);
|
|
406
|
+
* planner.plan({ contract, schema, policy, frameworkComponents: typedComponents });
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
function assertFrameworkComponentsCompatible(expectedFamilyId, expectedTargetId, frameworkComponents) {
|
|
410
|
+
for (let i = 0; i < frameworkComponents.length; i++) {
|
|
411
|
+
const component = frameworkComponents[i];
|
|
412
|
+
if (typeof component !== "object" || component === null) throw errorConfigValidation$1("frameworkComponents[]", { why: `Framework component at index ${i} must be an object` });
|
|
413
|
+
const record = component;
|
|
414
|
+
if (!Object.hasOwn(record, "kind")) throw errorConfigValidation$1("frameworkComponents[].kind", { why: `Framework component at index ${i} must have 'kind' property` });
|
|
415
|
+
const kind = record["kind"];
|
|
416
|
+
if (kind !== "target" && kind !== "adapter" && kind !== "extension" && kind !== "driver") throw errorConfigValidation$1("frameworkComponents[].kind", { why: `Framework component at index ${i} has invalid kind '${String(kind)}' (must be 'target', 'adapter', 'extension', or 'driver')` });
|
|
417
|
+
if (!Object.hasOwn(record, "familyId")) throw errorConfigValidation$1("frameworkComponents[].familyId", { why: `Framework component at index ${i} (kind: ${String(kind)}) must have 'familyId' property` });
|
|
418
|
+
const familyId = record["familyId"];
|
|
419
|
+
if (familyId !== expectedFamilyId) throw errorConfigValidation$1("frameworkComponents[].familyId", { why: `Framework component at index ${i} (kind: ${String(kind)}) has familyId '${String(familyId)}' but expected '${expectedFamilyId}'` });
|
|
420
|
+
if (!Object.hasOwn(record, "targetId")) throw errorConfigValidation$1("frameworkComponents[].targetId", { why: `Framework component at index ${i} (kind: ${String(kind)}) must have 'targetId' property` });
|
|
421
|
+
const targetId = record["targetId"];
|
|
422
|
+
if (targetId !== expectedTargetId) throw errorConfigValidation$1("frameworkComponents[].targetId", { why: `Framework component at index ${i} (kind: ${String(kind)}) has targetId '${String(targetId)}' but expected '${expectedTargetId}'` });
|
|
423
|
+
}
|
|
424
|
+
return frameworkComponents;
|
|
425
|
+
}
|
|
426
|
+
function isPlainObject(value) {
|
|
427
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
428
|
+
}
|
|
429
|
+
function sortDeep(value) {
|
|
430
|
+
if (Array.isArray(value)) return value.map(sortDeep);
|
|
431
|
+
if (!isPlainObject(value)) return value;
|
|
432
|
+
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
433
|
+
const next = {};
|
|
434
|
+
for (const [key, child] of entries) next[key] = sortDeep(child);
|
|
435
|
+
return next;
|
|
436
|
+
}
|
|
437
|
+
function sortDeepTyped(value) {
|
|
438
|
+
return sortDeep(value);
|
|
439
|
+
}
|
|
440
|
+
function extractExtensionPackMeta(component) {
|
|
441
|
+
const { kind, id, version, capabilities, types } = component;
|
|
442
|
+
const base = {
|
|
443
|
+
kind,
|
|
444
|
+
id,
|
|
445
|
+
familyId: component.familyId,
|
|
446
|
+
targetId: component.targetId,
|
|
447
|
+
version
|
|
448
|
+
};
|
|
449
|
+
if (capabilities) base["capabilities"] = capabilities;
|
|
450
|
+
if (types) if (types.codecTypes) {
|
|
451
|
+
const { controlPlaneHooks: _, codecDescriptors: _cd, ...cleanedCodecTypes } = types.codecTypes;
|
|
452
|
+
base["types"] = {
|
|
453
|
+
...types,
|
|
454
|
+
codecTypes: cleanedCodecTypes
|
|
455
|
+
};
|
|
456
|
+
} else base["types"] = types;
|
|
457
|
+
return base;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Enriches a raw contract with framework-derived metadata: capabilities from all component descriptors and extension pack metadata from extension descriptors. Produces deterministically sorted output.
|
|
461
|
+
*/
|
|
462
|
+
function enrichContract(ir, components) {
|
|
463
|
+
const mergedCapabilities = mergeCapabilityMatrices(ir.capabilities, components);
|
|
464
|
+
const extensionsMeta = {};
|
|
465
|
+
for (const component of components) if (component.kind === "extension") extensionsMeta[component.id] = extractExtensionPackMeta(component);
|
|
466
|
+
const extensions = Object.keys(extensionsMeta).length > 0 ? {
|
|
467
|
+
...ir.extensions,
|
|
468
|
+
...extensionsMeta
|
|
469
|
+
} : ir.extensions;
|
|
470
|
+
return {
|
|
471
|
+
...ir,
|
|
472
|
+
capabilities: sortDeepTyped(mergedCapabilities),
|
|
473
|
+
extensions: sortDeepTyped(extensions)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Resolves the absolute path to contract.json from the config.
|
|
478
|
+
*/
|
|
479
|
+
function resolveContractPath(config) {
|
|
480
|
+
if (config.contract?.output === void 0) throw errorRuntime("CONFIG.VALIDATION_FAILED", "config.contract.output is required to resolve the contract path", {
|
|
481
|
+
why: "CLI commands read the emitted contract from config.contract.output; the config has no value to read.",
|
|
482
|
+
fix: "Ensure your prisma.config.ts goes through `defineConfig()`, which normalises a default output when the provider supplies an input path, or set `contract.output` explicitly."
|
|
483
|
+
});
|
|
484
|
+
return resolve(config.contract.output);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Resolves the migrations directory and config path from CLI options.
|
|
488
|
+
* Shared by migrate, migration-plan, and migration-status.
|
|
489
|
+
*
|
|
490
|
+
* - `migrationsDir` is the project's top-level `migrations/` directory
|
|
491
|
+
* (the root that the aggregate loader walks for every contract space).
|
|
492
|
+
* - `appMigrationsDir` is the app subspace directory under it
|
|
493
|
+
* (`<migrationsDir>/<APP_SPACE_ID>/`). Every per-app reader / writer
|
|
494
|
+
* (`migration new`, `migration plan`, `migrate`,
|
|
495
|
+
* `migration status`, `migration show`, `migration ref`) operates on
|
|
496
|
+
* this directory. Extensions own their own `migrations/<spaceId>/`.
|
|
497
|
+
* - `refsDir` is the app's refs directory (`<appMigrationsDir>/refs/`).
|
|
498
|
+
* The framework does not maintain refs at the migrations root.
|
|
499
|
+
*
|
|
500
|
+
* `cwd` is the directory the command was invoked from; every relative path in
|
|
501
|
+
* the result is computed against it.
|
|
502
|
+
*/
|
|
503
|
+
function resolveMigrationPaths(configOption, config, cwd) {
|
|
504
|
+
const resolvedConfigPath = configOption ? resolve(cwd, configOption) : void 0;
|
|
505
|
+
const configPath = resolvedConfigPath ? relative(cwd, resolvedConfigPath) : "prisma.config.ts";
|
|
506
|
+
const migrationsDir = resolve(resolvedConfigPath ? resolve(resolvedConfigPath, "..") : cwd, config.migrations?.dir ?? "migrations");
|
|
507
|
+
const migrationsRelative = relative(cwd, migrationsDir);
|
|
508
|
+
const appMigrationsDir = spaceMigrationDirectory(migrationsDir, APP_SPACE_ID$1);
|
|
509
|
+
return {
|
|
510
|
+
configPath,
|
|
511
|
+
migrationsDir,
|
|
512
|
+
migrationsRelative,
|
|
513
|
+
appMigrationsDir,
|
|
514
|
+
appMigrationsRelative: relative(cwd, appMigrationsDir),
|
|
515
|
+
refsDir: resolve(appMigrationsDir, "refs")
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function collectDeclaredInvariants(graph) {
|
|
519
|
+
const declared = /* @__PURE__ */ new Set();
|
|
520
|
+
for (const edges of graph.forwardChain.values()) for (const edge of edges) for (const inv of edge.invariants) declared.add(inv);
|
|
521
|
+
return declared;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Maps a `MigrationEdge` to the structural-edge shape used in the
|
|
525
|
+
* `MIGRATION.NO_INVARIANT_PATH` error envelope. Shared between
|
|
526
|
+
* `migrate` and `migration status` so both commands surface
|
|
527
|
+
* the same JSON wire shape when an invariant-aware route is unsatisfiable.
|
|
528
|
+
*/
|
|
529
|
+
function toStructuralEdge(edge) {
|
|
530
|
+
return {
|
|
531
|
+
dirName: edge.dirName,
|
|
532
|
+
migrationHash: edge.migrationHash,
|
|
533
|
+
from: edge.from,
|
|
534
|
+
to: edge.to,
|
|
535
|
+
invariants: edge.invariants
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function targetSupportsMigrations(target) {
|
|
539
|
+
return hasMigrations(target);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Hangs up without letting the hang-up decide the command's result. A rejection out of a
|
|
543
|
+
* `finally` replaces the value the `try`/`catch` already returned, so an unguarded close turns a
|
|
544
|
+
* mapped connection error into an unmapped one — and that is the case it hits most, because a
|
|
545
|
+
* `connect()` that failed leaves nothing to close.
|
|
546
|
+
*/
|
|
547
|
+
async function closeQuietly(client) {
|
|
548
|
+
try {
|
|
549
|
+
await client.close();
|
|
550
|
+
} catch {}
|
|
551
|
+
}
|
|
552
|
+
function getTargetMigrations(target) {
|
|
553
|
+
return hasMigrations(target) ? target.migrations : void 0;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Reads and parses contract.json, validating the framework-level envelope
|
|
557
|
+
* fields (storageHash, schemaVersion, target, targetFamily).
|
|
558
|
+
*
|
|
559
|
+
* Family-specific validation (storage structure, codec mappings, etc.)
|
|
560
|
+
* happens downstream in the control client via the family instance.
|
|
561
|
+
*/
|
|
562
|
+
async function readContractEnvelope(config) {
|
|
563
|
+
const contractPath = resolveContractPath(config);
|
|
564
|
+
const content = await readFile(contractPath, "utf-8");
|
|
565
|
+
const json = JSON.parse(content);
|
|
566
|
+
const { schemaVersion, target, targetFamily, profileHash } = json;
|
|
567
|
+
const storageHash = json["storage"]?.["storageHash"];
|
|
568
|
+
if (typeof storageHash !== "string") throw new CliStructuredError$1("CONTRACT.VALIDATION_FAILED", `Contract at ${relative(process.cwd(), contractPath)} is missing a valid storage.storageHash. Run \`prisma-cli contract emit\` to regenerate.`, { where: { path: contractPath } });
|
|
569
|
+
if (typeof schemaVersion !== "string") throw new CliStructuredError$1("CONTRACT.VALIDATION_FAILED", `Contract at ${relative(process.cwd(), contractPath)} is missing schemaVersion.`, { where: { path: contractPath } });
|
|
570
|
+
if (typeof target !== "string") throw new CliStructuredError$1("CONTRACT.VALIDATION_FAILED", `Contract at ${relative(process.cwd(), contractPath)} is missing target.`, { where: { path: contractPath } });
|
|
571
|
+
if (typeof targetFamily !== "string") throw new CliStructuredError$1("CONTRACT.VALIDATION_FAILED", `Contract at ${relative(process.cwd(), contractPath)} is missing targetFamily.`, { where: { path: contractPath } });
|
|
572
|
+
return {
|
|
573
|
+
...json,
|
|
574
|
+
storageHash,
|
|
575
|
+
schemaVersion,
|
|
576
|
+
target,
|
|
577
|
+
targetFamily,
|
|
578
|
+
...typeof profileHash === "string" ? { profileHash } : {}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Masks credentials in a database connection URL.
|
|
583
|
+
* Handles standard URLs (username + password + query params) and libpq-style key=value strings.
|
|
584
|
+
*/
|
|
585
|
+
function maskConnectionUrl(url) {
|
|
586
|
+
try {
|
|
587
|
+
const parsed = new URL(url);
|
|
588
|
+
if (parsed.username) parsed.username = "****";
|
|
589
|
+
if (parsed.password) parsed.password = "****";
|
|
590
|
+
for (const key of [...parsed.searchParams.keys()]) if (/password/i.test(key)) parsed.searchParams.set(key, "****");
|
|
591
|
+
return parsed.toString();
|
|
592
|
+
} catch {
|
|
593
|
+
return url.replace(/password\s*=\s*\S+/gi, "password=****").replace(/user\s*=\s*\S+/gi, "user=****");
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Strips raw connection URL fragments from an error message to prevent credential leakage.
|
|
598
|
+
* Call this before surfacing driver errors to the user.
|
|
599
|
+
*/
|
|
600
|
+
function sanitizeErrorMessage(message, connectionUrl) {
|
|
601
|
+
if (!connectionUrl) return message;
|
|
602
|
+
try {
|
|
603
|
+
const parsed = new URL(connectionUrl);
|
|
604
|
+
let sanitized = message;
|
|
605
|
+
sanitized = sanitized.replaceAll(connectionUrl, maskConnectionUrl(connectionUrl));
|
|
606
|
+
if (parsed.password) sanitized = sanitized.replaceAll(parsed.password, "****");
|
|
607
|
+
if (parsed.username) sanitized = sanitized.replaceAll(parsed.username, "****");
|
|
608
|
+
return sanitized;
|
|
609
|
+
} catch {
|
|
610
|
+
return message.replace(/password\s*=\s*\S+/gi, "password=****").replace(/user\s*=\s*\S+/gi, "user=****");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Project the CLI's `Config.extensions` array into the canonical
|
|
615
|
+
* {@link ExtensionPackInput} shape. The single `as ExtensionPackLike`
|
|
616
|
+
* structural cast in the CLI lives inside this function.
|
|
617
|
+
*/
|
|
618
|
+
function toExtensionInputs(extensions) {
|
|
619
|
+
return extensions.map((raw) => {
|
|
620
|
+
const pack = raw;
|
|
621
|
+
if (pack.contractSpace === void 0) return {
|
|
622
|
+
id: pack.id,
|
|
623
|
+
targetId: pack.targetId
|
|
624
|
+
};
|
|
625
|
+
return {
|
|
626
|
+
id: pack.id,
|
|
627
|
+
targetId: pack.targetId,
|
|
628
|
+
contractSpace: {
|
|
629
|
+
contractJson: pack.contractSpace.contractJson,
|
|
630
|
+
headRef: pack.contractSpace.headRef,
|
|
631
|
+
migrations: pack.contractSpace.migrations ?? []
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Minimal aggregate-loader projection that extracts `id` + `targetId`
|
|
638
|
+
* from raw extension pack descriptors **without invoking any
|
|
639
|
+
* `contractSpace` accessor**. Inspects the own-property descriptor so
|
|
640
|
+
* that getter-backed `contractSpace` declarations are detected but
|
|
641
|
+
* never called.
|
|
642
|
+
*
|
|
643
|
+
* Inclusion semantics match {@link toDeclaredExtensions}: a data
|
|
644
|
+
* property whose value is explicitly `undefined` is treated as "no
|
|
645
|
+
* contract-space declaration" and skipped, mirroring the
|
|
646
|
+
* `pack.contractSpace === undefined` check used on canonicalised
|
|
647
|
+
* inputs. Prototype-chain `contractSpace` properties (no own
|
|
648
|
+
* descriptor) are also skipped.
|
|
649
|
+
*
|
|
650
|
+
* This variant must be used by `buildContractSpaceAggregate` so that
|
|
651
|
+
* the aggregate path (including `db verify`) never reads
|
|
652
|
+
* `contractSpace.contractJson` from extension descriptors — the loader
|
|
653
|
+
* always reads the contract from on-disk artefacts instead.
|
|
654
|
+
*/
|
|
655
|
+
function toDeclaredExtensionsFromRaw(extensions) {
|
|
656
|
+
const entries = [];
|
|
657
|
+
for (const raw of extensions) {
|
|
658
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
659
|
+
const descriptor = Object.getOwnPropertyDescriptor(raw, "contractSpace");
|
|
660
|
+
if (descriptor === void 0) continue;
|
|
661
|
+
if ("value" in descriptor && descriptor.value === void 0) continue;
|
|
662
|
+
const pack = raw;
|
|
663
|
+
entries.push({
|
|
664
|
+
id: pack.id,
|
|
665
|
+
targetId: pack.targetId
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return entries;
|
|
669
|
+
}
|
|
670
|
+
const CONTRACT_SPACES_DOCS_URL = "https://pris.ly/contract-spaces";
|
|
671
|
+
function contractSpaceViolationError(summary, options) {
|
|
672
|
+
return new CliStructuredError$1("MIGRATION.CONTRACT_SPACE_VIOLATION", summary, {
|
|
673
|
+
why: options.why,
|
|
674
|
+
fix: options.fix,
|
|
675
|
+
docsUrl: CONTRACT_SPACES_DOCS_URL,
|
|
676
|
+
meta: { violations: options.violations }
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Build the `MIGRATION.CONTRACT_SPACE_VIOLATION` structured-error envelope for a contract-space
|
|
681
|
+
* target mismatch. Shared between the declared-extension precheck (the
|
|
682
|
+
* descriptor's configured target disagrees with the project target) and
|
|
683
|
+
* the on-disk-contract check surfaced by `checkIntegrity`.
|
|
684
|
+
*/
|
|
685
|
+
function targetMismatchError(spaceId, expected, actual) {
|
|
686
|
+
return contractSpaceViolationError(`Contract-space target mismatch for "${spaceId}"`, {
|
|
687
|
+
why: `Space "${spaceId}" targets "${actual}" but the project's adapter targets "${expected}".`,
|
|
688
|
+
fix: "Update the extension descriptor to target the configured database, or change the project adapter.",
|
|
689
|
+
violations: [{
|
|
690
|
+
kind: "targetMismatch",
|
|
691
|
+
spaceId,
|
|
692
|
+
expected,
|
|
693
|
+
actual
|
|
694
|
+
}]
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Human-readable detail for an integrity violation, used as the `why`
|
|
699
|
+
* of the `integrityFailure` envelope. Mirrors the messages the prior
|
|
700
|
+
* throw-on-load loader produced so downstream consumers see the same
|
|
701
|
+
* text for the same on-disk state.
|
|
702
|
+
*/
|
|
703
|
+
function describeIntegrityViolation(violation) {
|
|
704
|
+
switch (violation.kind) {
|
|
705
|
+
case "hashMismatch": return `Migration "${violation.dirName}" stored hash "${violation.stored}" does not match computed hash "${violation.computed}".`;
|
|
706
|
+
case "providedInvariantsMismatch": return `Migration "${violation.dirName}" providedInvariants in migration.json disagrees with ops.json.`;
|
|
707
|
+
case "packageUnloadable": return `Migration "${violation.dirName}" could not be loaded: ${violation.detail}`;
|
|
708
|
+
case "sameSourceAndTarget": return `Migration "${violation.dirName}" has source equal to target (${violation.hash}) with no data invariant — a true no-op self-edge.`;
|
|
709
|
+
case "headRefMissing": return `Head ref \`refs/head.json\` is missing for contract space "${violation.spaceId}".`;
|
|
710
|
+
case "headRefNotInGraph": return `Head ref ${violation.hash} for contract space "${violation.spaceId}" is not present in the migration graph.`;
|
|
711
|
+
case "refUnreadable": return `Ref "${violation.refName}" for contract space "${violation.spaceId}" is unreadable: ${violation.detail}`;
|
|
712
|
+
case "duplicateMigrationHash": return `Multiple migrations in space "${violation.spaceId}" share migrationHash "${violation.migrationHash}" (${violation.dirNames.join(", ")}).`;
|
|
713
|
+
default: {
|
|
714
|
+
const spaceId = "spaceId" in violation ? violation.spaceId : "*";
|
|
715
|
+
return `Integrity violation "${violation.kind}" for contract space "${spaceId}".`;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Map the integrity violations `checkIntegrity` reports into a single
|
|
721
|
+
* CLI structured-error envelope, preserving the error codes the prior
|
|
722
|
+
* throw-on-load loader emitted: `MIGRATION.CONTRACT_SPACE_LAYOUT_VIOLATION`
|
|
723
|
+
* (layout drift, bundled) and `MIGRATION.CONTRACT_SPACE_VIOLATION` (target /
|
|
724
|
+
* disjointness / contract-validation / structural integrity). Returns
|
|
725
|
+
* `null` when there is nothing to refuse on.
|
|
726
|
+
*
|
|
727
|
+
* Precedence reproduces the prior loader's first-failure ordering:
|
|
728
|
+
* layout drift first (every offence bundled into one envelope), then
|
|
729
|
+
* target mismatch, then disjointness, then a contract-validation
|
|
730
|
+
* failure, then any remaining structural integrity violation.
|
|
731
|
+
*/
|
|
732
|
+
function mapIntegrityViolations(violations) {
|
|
733
|
+
if (violations.length === 0) return null;
|
|
734
|
+
const layout = violations.filter((v) => v.kind === "orphanSpaceDir" || v.kind === "declaredButUnmigrated");
|
|
735
|
+
if (layout.length > 0) {
|
|
736
|
+
const lines = layout.map((v) => `- [${v.kind}] ${v.spaceId}`);
|
|
737
|
+
return new CliStructuredError$1("MIGRATION.CONTRACT_SPACE_LAYOUT_VIOLATION", layout.length === 1 ? "Contract-space layout violation detected" : `Contract-space layout violations detected (${layout.length})`, {
|
|
738
|
+
why: `The on-disk \`migrations/\` directory and your \`extensions\` declaration are not in agreement.\n${lines.join("\n")}`,
|
|
739
|
+
fix: "Declare the extension in `extensions` and re-emit its contract-space artefacts, or remove the orphan `migrations/<space>` directory.",
|
|
740
|
+
docsUrl: CONTRACT_SPACES_DOCS_URL,
|
|
741
|
+
meta: { violations: layout }
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
const targetMismatch = violations.find((v) => v.kind === "targetMismatch");
|
|
745
|
+
if (targetMismatch && targetMismatch.kind === "targetMismatch") return targetMismatchError(targetMismatch.spaceId, targetMismatch.expected, targetMismatch.actual);
|
|
746
|
+
const disjointness = violations.find((v) => v.kind === "disjointness");
|
|
747
|
+
if (disjointness && disjointness.kind === "disjointness") return contractSpaceViolationError(`Contract-space disjointness violation: storage element "${disjointness.element}" claimed by multiple spaces`, {
|
|
748
|
+
why: `Spaces ${disjointness.claimedBy.map((s) => `"${s}"`).join(", ")} all claim the storage element "${disjointness.element}". Each storage element must be owned by exactly one contract space.`,
|
|
749
|
+
fix: "Update the conflicting contracts so each storage element is claimed by exactly one space.",
|
|
750
|
+
violations: [disjointness]
|
|
751
|
+
});
|
|
752
|
+
const contractUnreadable = violations.find((v) => v.kind === "contractUnreadable");
|
|
753
|
+
if (contractUnreadable && contractUnreadable.kind === "contractUnreadable") return contractSpaceViolationError(`Contract-space contract validation failed for "${contractUnreadable.spaceId}"`, {
|
|
754
|
+
why: contractUnreadable.detail,
|
|
755
|
+
fix: "Re-emit the extension contract with `prisma-cli contract emit`, or fix the extension pack descriptor producing the invalid contract.",
|
|
756
|
+
violations: [contractUnreadable]
|
|
757
|
+
});
|
|
758
|
+
const structural = violations[0];
|
|
759
|
+
return contractSpaceViolationError(`Contract-space integrity failure for "${"spaceId" in structural ? structural.spaceId : "*"}"`, {
|
|
760
|
+
why: describeIntegrityViolation(structural),
|
|
761
|
+
fix: "Re-emit the affected migration package(s) or restore the on-disk `migrations/` directory from version control.",
|
|
762
|
+
violations: [structural]
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
function declaredExtensionsFromInputs(extensions) {
|
|
766
|
+
return toDeclaredExtensionsFromRaw(extensions);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Reject extension descriptors whose configured target disagrees with
|
|
770
|
+
* the project target before any on-disk read.
|
|
771
|
+
*/
|
|
772
|
+
function refuseDeclaredExtensionTargetMismatch(inputs) {
|
|
773
|
+
for (const declared of declaredExtensionsFromInputs(inputs.extensions)) if (declared.targetId !== inputs.targetId) return targetMismatchError(declared.id, inputs.targetId, declared.targetId);
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Load the tolerant {@link ContractSpaceAggregate} once at the CLI
|
|
778
|
+
* surface. Construction never throws on disk content; callers query
|
|
779
|
+
* {@link ContractSpaceAggregate.app} / extension facets instead of
|
|
780
|
+
* re-reading `migrations/`.
|
|
781
|
+
*/
|
|
782
|
+
async function loadContractSpaceAggregateForCli(inputs) {
|
|
783
|
+
const targetFailure = refuseDeclaredExtensionTargetMismatch(inputs);
|
|
784
|
+
if (targetFailure) return notOk(targetFailure);
|
|
785
|
+
return ok(await loadContractSpaceAggregate({
|
|
786
|
+
migrationsDir: inputs.migrationsDir,
|
|
787
|
+
deserializeContract: inputs.deserializeContract,
|
|
788
|
+
appContract: inputs.appContract
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Run `checkIntegrity` on a loaded aggregate and map violations into
|
|
793
|
+
* the contract-space refusal envelope, or return `null` when the model
|
|
794
|
+
* is acceptable for the requested check scope.
|
|
795
|
+
*/
|
|
796
|
+
function refuseContractSpaceIntegrity(aggregate, options) {
|
|
797
|
+
return mapIntegrityViolations(aggregate.checkIntegrity(options));
|
|
798
|
+
}
|
|
799
|
+
const PACKAGE_CORRUPTION_KINDS = /* @__PURE__ */ new Set([
|
|
800
|
+
"hashMismatch",
|
|
801
|
+
"providedInvariantsMismatch",
|
|
802
|
+
"packageUnloadable"
|
|
803
|
+
]);
|
|
804
|
+
/**
|
|
805
|
+
* Reader-subset integrity refusal for `migration status`: package
|
|
806
|
+
* corruptions only (`hashMismatch`, `providedInvariantsMismatch`,
|
|
807
|
+
* `packageUnloadable`).
|
|
808
|
+
*/
|
|
809
|
+
function refusePackageCorruptionOnAggregate(aggregate) {
|
|
810
|
+
return mapIntegrityViolations(aggregate.checkIntegrity().filter((v) => PACKAGE_CORRUPTION_KINDS.has(v.kind)));
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Construct the tolerant {@link ContractSpaceAggregate} at the CLI
|
|
814
|
+
* surface and apply the explicit integrity refusal.
|
|
815
|
+
*
|
|
816
|
+
* App-space migration packages are read from `migrations/<app>/` by the
|
|
817
|
+
* loader itself; callers no longer thread them through.
|
|
818
|
+
*/
|
|
819
|
+
async function buildContractSpaceAggregate(inputs) {
|
|
820
|
+
const declaredExtensions = declaredExtensionsFromInputs(inputs.extensions);
|
|
821
|
+
const loaded = await loadContractSpaceAggregateForCli(inputs);
|
|
822
|
+
if (!loaded.ok) return loaded;
|
|
823
|
+
const failure = refuseContractSpaceIntegrity(loaded.value, {
|
|
824
|
+
declaredExtensions,
|
|
825
|
+
checkContracts: true
|
|
826
|
+
});
|
|
827
|
+
if (failure) return notOk(failure);
|
|
828
|
+
return ok(loaded.value);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Build a minimal app {@link Contract} carrying only the project's
|
|
832
|
+
* contract-identity (`storage.storageHash` + `target` / `targetFamily`)
|
|
833
|
+
* when the real `contract.json` is absent or undeserializable.
|
|
834
|
+
*
|
|
835
|
+
* `loadContractSpaceAggregate` requires an `appContract` to synthesise the
|
|
836
|
+
* app space's head ref from its storage hash. Read commands query only that
|
|
837
|
+
* hash and the target — never `models` — so an empty-`models` stand-in is
|
|
838
|
+
* sufficient for them. It is *not* a valid contract for any consumer that
|
|
839
|
+
* reads schema, which is why this is confined to the read-aggregate path.
|
|
840
|
+
*/
|
|
841
|
+
function appContractStandInFromIdentity(args) {
|
|
842
|
+
return blindCast({
|
|
843
|
+
storage: { storageHash: args.contractHash },
|
|
844
|
+
schemaVersion: "0.0.0",
|
|
845
|
+
target: args.targetId,
|
|
846
|
+
targetFamily: args.targetFamily,
|
|
847
|
+
models: {},
|
|
848
|
+
profileHash: EMPTY_CONTRACT_HASH
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
async function loadContractRawSafely(config) {
|
|
852
|
+
try {
|
|
853
|
+
const raw = await readFile(resolveContractPath(config), "utf-8");
|
|
854
|
+
return JSON.parse(raw);
|
|
855
|
+
} catch {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Tolerant {@link ContractSpaceAggregate} assembly for read-only CLI
|
|
861
|
+
* commands. No integrity gate — callers query `aggregate.app` (or other
|
|
862
|
+
* facets) without re-reading `migrations/`. When `contract.json` is absent
|
|
863
|
+
* or undeserializable, the app contract falls back to an identity-only
|
|
864
|
+
* stand-in ({@link appContractStandInFromIdentity}), so these commands
|
|
865
|
+
* load without requiring a readable contract.
|
|
866
|
+
*/
|
|
867
|
+
async function buildReadAggregate(config, options) {
|
|
868
|
+
let contractHash = EMPTY_CONTRACT_HASH;
|
|
869
|
+
try {
|
|
870
|
+
contractHash = (await readContractEnvelope(config)).storageHash;
|
|
871
|
+
} catch {}
|
|
872
|
+
try {
|
|
873
|
+
const contractRawForAggregate = await loadContractRawSafely(config);
|
|
874
|
+
const stack = createControlStack(config);
|
|
875
|
+
const familyInstance = config.family.create(stack);
|
|
876
|
+
const deserializeContract = (json) => familyInstance.deserializeContract(json);
|
|
877
|
+
let appContractForLoad = appContractStandInFromIdentity({
|
|
878
|
+
contractHash,
|
|
879
|
+
targetId: config.target.id,
|
|
880
|
+
targetFamily: config.target.familyId
|
|
881
|
+
});
|
|
882
|
+
if (contractRawForAggregate !== null) try {
|
|
883
|
+
appContractForLoad = deserializeContract(contractRawForAggregate);
|
|
884
|
+
} catch {}
|
|
885
|
+
const loaded = await loadContractSpaceAggregateForCli({
|
|
886
|
+
targetId: config.target.id,
|
|
887
|
+
migrationsDir: options.migrationsDir,
|
|
888
|
+
appContract: appContractForLoad,
|
|
889
|
+
extensions: config.extensions ?? [],
|
|
890
|
+
deserializeContract
|
|
891
|
+
});
|
|
892
|
+
if (!loaded.ok) return loaded;
|
|
893
|
+
return ok({
|
|
894
|
+
aggregate: loaded.value,
|
|
895
|
+
contractHash
|
|
896
|
+
});
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (MigrationToolsError.is(error)) return notOk(error);
|
|
899
|
+
return notOk(errorUnexpected(error instanceof Error ? error.message : String(error), { why: `Failed to read migrations directory: ${error instanceof Error ? error.message : String(error)}` }));
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Strips operation objects to their public shape (id, label, operationClass).
|
|
904
|
+
* Used at the API boundary to avoid leaking internal fields (precheck, execute, postcheck, etc.).
|
|
905
|
+
*/
|
|
906
|
+
function stripOperations(operations) {
|
|
907
|
+
return operations.map((op) => ({
|
|
908
|
+
id: op.id,
|
|
909
|
+
label: op.label,
|
|
910
|
+
operationClass: op.operationClass
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Content identity of a `db update` plan: what it does (the ordered display
|
|
915
|
+
* operations) and where it lands (the destination storage hash). A
|
|
916
|
+
* `DESTRUCTIVE_CHANGES` refusal names the refused plan by this hash, and the
|
|
917
|
+
* apply that carries the consent back recomputes it to prove it is still
|
|
918
|
+
* applying the plan that was consented to.
|
|
919
|
+
*/
|
|
920
|
+
function computePlanHash(plan) {
|
|
921
|
+
const canonical = canonicalizeJson({
|
|
922
|
+
operations: plan.operations,
|
|
923
|
+
destination: { storageHash: plan.destination.storageHash }
|
|
924
|
+
});
|
|
925
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Span id emitted via `onProgress` for the run phase. Stable
|
|
929
|
+
* identifier consumed by the structured-output renderer and by tests.
|
|
930
|
+
*/
|
|
931
|
+
const RUN_SPAN_ID = "apply";
|
|
932
|
+
/**
|
|
933
|
+
* Runner-driving tail shared by every run caller — `db init`,
|
|
934
|
+
* `db update`, and `migrate`. Consumes already-resolved per-space
|
|
935
|
+
* plans (the planner-vs-replay distinction is owned by the caller) and
|
|
936
|
+
* dispatches them to the runner in canonical order.
|
|
937
|
+
*
|
|
938
|
+
* Marker advancement is part of the runner's per-space transaction
|
|
939
|
+
* (the SQL family runner writes the marker as the last step of each
|
|
940
|
+
* space's transaction), so this primitive does not advance markers
|
|
941
|
+
* separately — by the time `execute` returns ok, every
|
|
942
|
+
* space's marker has been advanced to its plan's destination.
|
|
943
|
+
*
|
|
944
|
+
* Span emission (`spanStart 'apply'` / `spanEnd 'apply'`) is owned here
|
|
945
|
+
* so callers don't have to duplicate it; the `action` field on each
|
|
946
|
+
* progress event is taken from the caller's `action` argument.
|
|
947
|
+
*/
|
|
948
|
+
async function runMigration(inputs) {
|
|
949
|
+
const { aggregate, perSpacePlans, applyOrder, driver, familyInstance, migrations, frameworkComponents, policy, action, onProgress } = inputs;
|
|
950
|
+
const orderedResolutions = collectOrdered(applyOrder, perSpacePlans);
|
|
951
|
+
const runner = migrations.createRunner(familyInstance);
|
|
952
|
+
onProgress?.({
|
|
953
|
+
action,
|
|
954
|
+
kind: "spanStart",
|
|
955
|
+
spanId: RUN_SPAN_ID,
|
|
956
|
+
label: progressLabelForAction(action)
|
|
957
|
+
});
|
|
958
|
+
const perSpaceOptions = orderedResolutions.map((r) => ({
|
|
959
|
+
space: r.spaceId,
|
|
960
|
+
plan: r.entry.plan,
|
|
961
|
+
driver,
|
|
962
|
+
destinationContract: r.entry.destinationContract,
|
|
963
|
+
policy,
|
|
964
|
+
frameworkComponents,
|
|
965
|
+
migrationEdges: r.entry.migrationEdges,
|
|
966
|
+
strictVerification: false
|
|
967
|
+
}));
|
|
968
|
+
const runnerResult = await runner.execute({
|
|
969
|
+
driver,
|
|
970
|
+
perSpaceOptions
|
|
971
|
+
});
|
|
972
|
+
if (!runnerResult.ok) {
|
|
973
|
+
onProgress?.({
|
|
974
|
+
action,
|
|
975
|
+
kind: "spanEnd",
|
|
976
|
+
spanId: RUN_SPAN_ID,
|
|
977
|
+
outcome: "error"
|
|
978
|
+
});
|
|
979
|
+
return notOk({
|
|
980
|
+
summary: runnerResult.failure.summary,
|
|
981
|
+
...ifDefined("why", runnerResult.failure.why),
|
|
982
|
+
meta: {
|
|
983
|
+
...runnerResult.failure.meta ?? {},
|
|
984
|
+
failingSpace: runnerResult.failure.failingSpace,
|
|
985
|
+
runnerErrorCode: runnerResult.failure.code
|
|
986
|
+
},
|
|
987
|
+
cause: runnerResult.failure
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
onProgress?.({
|
|
991
|
+
action,
|
|
992
|
+
kind: "spanEnd",
|
|
993
|
+
spanId: RUN_SPAN_ID,
|
|
994
|
+
outcome: "ok"
|
|
995
|
+
});
|
|
996
|
+
return ok({
|
|
997
|
+
orderedResolutions,
|
|
998
|
+
totalOpsPlanned: runnerResult.value.perSpaceResults.reduce((sum, r) => sum + r.value.operationsPlanned, 0),
|
|
999
|
+
totalOpsExecuted: runnerResult.value.perSpaceResults.reduce((sum, r) => sum + r.value.operationsExecuted, 0),
|
|
1000
|
+
perSpace: buildPerSpaceBreakdown(orderedResolutions, aggregate.app.spaceId, { includeMarkers: true })
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Project the planner's per-space resolutions into the
|
|
1005
|
+
* `PerSpaceExecutionEntry[]` shape the CLI surfaces.
|
|
1006
|
+
*
|
|
1007
|
+
* `includeMarkers` is `true` for apply-mode (each space's marker is
|
|
1008
|
+
* the `destination.storageHash` of its plan, which the runner
|
|
1009
|
+
* advances as the last step of each space's transaction) and `false`
|
|
1010
|
+
* for plan-mode (no marker has been written yet).
|
|
1011
|
+
*
|
|
1012
|
+
* Exported alongside {@link runMigration} so plan-mode callers can
|
|
1013
|
+
* assemble the same per-space block without going through the runner.
|
|
1014
|
+
*/
|
|
1015
|
+
function buildPerSpaceBreakdown(orderedResolutions, appSpaceId, options) {
|
|
1016
|
+
return orderedResolutions.map((r) => {
|
|
1017
|
+
const operations = r.entry.displayOps.map((op) => ({
|
|
1018
|
+
id: op.id,
|
|
1019
|
+
label: op.label,
|
|
1020
|
+
operationClass: op.operationClass
|
|
1021
|
+
}));
|
|
1022
|
+
const base = {
|
|
1023
|
+
spaceId: r.spaceId,
|
|
1024
|
+
kind: r.spaceId === appSpaceId ? "app" : "extension",
|
|
1025
|
+
operations
|
|
1026
|
+
};
|
|
1027
|
+
if (!options.includeMarkers) return base;
|
|
1028
|
+
return {
|
|
1029
|
+
...base,
|
|
1030
|
+
marker: { storageHash: r.entry.plan.destination.storageHash }
|
|
1031
|
+
};
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Materialise the `applyOrder` ordering into resolved per-space
|
|
1036
|
+
* entries. Throws if the planner output is missing a contract space listed
|
|
1037
|
+
* in `applyOrder` — a wiring bug that should never reach runtime.
|
|
1038
|
+
*
|
|
1039
|
+
* Exported so callers building their own success envelopes after a
|
|
1040
|
+
* plan-mode dispatch can replay the same ordering.
|
|
1041
|
+
*/
|
|
1042
|
+
function collectOrdered(applyOrder, perSpace) {
|
|
1043
|
+
return applyOrder.map((spaceId) => {
|
|
1044
|
+
const entry = perSpace.get(spaceId);
|
|
1045
|
+
if (!entry) throw new InternalError(`planner output missing per-space plan for "${spaceId}"`);
|
|
1046
|
+
return {
|
|
1047
|
+
spaceId,
|
|
1048
|
+
entry
|
|
1049
|
+
};
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Action-appropriate label for the `spanStart` event the run
|
|
1054
|
+
* primitive emits. `runMigration` is shared by `db init`, `db update`,
|
|
1055
|
+
* and `migrate`; the span label tracks the user-visible action
|
|
1056
|
+
* so structured-progress output reads naturally for each surface.
|
|
1057
|
+
*/
|
|
1058
|
+
function progressLabelForAction(action) {
|
|
1059
|
+
switch (action) {
|
|
1060
|
+
case "dbInit": return "Initialising database across spaces";
|
|
1061
|
+
case "dbUpdate": return "Updating database across spaces";
|
|
1062
|
+
case "migrate": return "Running migration plan across spaces";
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Span IDs emitted via `onProgress` during the run flow.
|
|
1067
|
+
* Stable identifiers consumed by the structured-output renderer and by
|
|
1068
|
+
* tests asserting on span ids. The `apply` span itself is owned by
|
|
1069
|
+
* the {@link runMigration} primitive — only the introspect / plan
|
|
1070
|
+
* spans are emitted directly here.
|
|
1071
|
+
*/
|
|
1072
|
+
const SPAN_IDS$1 = {
|
|
1073
|
+
introspect: "introspect",
|
|
1074
|
+
plan: "plan"
|
|
1075
|
+
};
|
|
1076
|
+
/**
|
|
1077
|
+
* Loader → planner → runner pipeline shared by `db init` and `db update`.
|
|
1078
|
+
*
|
|
1079
|
+
* The pipeline:
|
|
1080
|
+
*
|
|
1081
|
+
* 1. **Load**: build a {@link ContractSpaceAggregate} from the descriptor
|
|
1082
|
+
* set + on-disk on-disk artefacts. Any layout / drift / disjointness /
|
|
1083
|
+
* integrity violation short-circuits with a structured error.
|
|
1084
|
+
* 2. **Read DB state**: marker rows (`familyInstance.readAllMarkers`)
|
|
1085
|
+
* + introspected schema (`familyInstance.introspect`).
|
|
1086
|
+
* 3. **Plan**: {@link planMigration} chooses `resolveRecordedPath` vs
|
|
1087
|
+
* `planFromDiff` per space according to `callerPolicy.ignoreGraphFor`.
|
|
1088
|
+
* The app space is forced through `planFromDiff` (today's daily-driver
|
|
1089
|
+
* behaviour); every extension space walks its on-disk graph via
|
|
1090
|
+
* `resolveRecordedPath`.
|
|
1091
|
+
* 4. **Apply** (when `mode === 'apply'`): every per-space `MigrationPlan`
|
|
1092
|
+
* feeds into the runner's `execute` — one outer
|
|
1093
|
+
* transaction across every space; failure on any space rolls back
|
|
1094
|
+
* every space's writes.
|
|
1095
|
+
*/
|
|
1096
|
+
async function executeRun(options) {
|
|
1097
|
+
const { driver, adapter, familyInstance, contract, mode, migrations, frameworkComponents, migrationsDir, extensions, targetId, policy, action, onProgress } = options;
|
|
1098
|
+
const loaded = await buildContractSpaceAggregate({
|
|
1099
|
+
targetId,
|
|
1100
|
+
migrationsDir,
|
|
1101
|
+
appContract: contract,
|
|
1102
|
+
extensions,
|
|
1103
|
+
deserializeContract: (json) => familyInstance.deserializeContract(json)
|
|
1104
|
+
});
|
|
1105
|
+
if (!loaded.ok) throw loaded.failure;
|
|
1106
|
+
const aggregate = loaded.value;
|
|
1107
|
+
const markerRows = await familyInstance.readAllMarkers({ driver });
|
|
1108
|
+
if (mode === "apply") {
|
|
1109
|
+
const orphanMarkerError = detectOrphanMarkers(aggregate, markerRows);
|
|
1110
|
+
if (orphanMarkerError !== null) throw orphanMarkerError;
|
|
1111
|
+
}
|
|
1112
|
+
onProgress?.({
|
|
1113
|
+
action,
|
|
1114
|
+
kind: "spanStart",
|
|
1115
|
+
spanId: SPAN_IDS$1.introspect,
|
|
1116
|
+
label: "Introspecting database schema"
|
|
1117
|
+
});
|
|
1118
|
+
const schemaIR = await familyInstance.introspect({
|
|
1119
|
+
driver,
|
|
1120
|
+
contract: collectAggregateNamespaces(aggregate)
|
|
1121
|
+
});
|
|
1122
|
+
onProgress?.({
|
|
1123
|
+
action,
|
|
1124
|
+
kind: "spanEnd",
|
|
1125
|
+
spanId: SPAN_IDS$1.introspect,
|
|
1126
|
+
outcome: "ok"
|
|
1127
|
+
});
|
|
1128
|
+
onProgress?.({
|
|
1129
|
+
action,
|
|
1130
|
+
kind: "spanStart",
|
|
1131
|
+
spanId: SPAN_IDS$1.plan,
|
|
1132
|
+
label: "Planning migration"
|
|
1133
|
+
});
|
|
1134
|
+
const planResult = await planMigration({
|
|
1135
|
+
aggregate,
|
|
1136
|
+
currentDBState: {
|
|
1137
|
+
markersBySpaceId: markerRows,
|
|
1138
|
+
schemaIntrospection: schemaIR
|
|
1139
|
+
},
|
|
1140
|
+
adapter,
|
|
1141
|
+
migrations,
|
|
1142
|
+
frameworkComponents,
|
|
1143
|
+
callerPolicy: { ignoreGraphFor: /* @__PURE__ */ new Set([aggregate.app.spaceId]) },
|
|
1144
|
+
operationPolicy: policy
|
|
1145
|
+
});
|
|
1146
|
+
if (!planResult.ok) {
|
|
1147
|
+
onProgress?.({
|
|
1148
|
+
action,
|
|
1149
|
+
kind: "spanEnd",
|
|
1150
|
+
spanId: SPAN_IDS$1.plan,
|
|
1151
|
+
outcome: "error"
|
|
1152
|
+
});
|
|
1153
|
+
return mapPlannerError(planResult.failure);
|
|
1154
|
+
}
|
|
1155
|
+
onProgress?.({
|
|
1156
|
+
action,
|
|
1157
|
+
kind: "spanEnd",
|
|
1158
|
+
spanId: SPAN_IDS$1.plan,
|
|
1159
|
+
outcome: "ok"
|
|
1160
|
+
});
|
|
1161
|
+
const orderedResolutions = collectOrdered(planResult.value.applyOrder, planResult.value.perSpace);
|
|
1162
|
+
const plannerWarnings = aggregatePlannerWarnings(orderedResolutions);
|
|
1163
|
+
const appResolution = orderedResolutions.find((r) => r.spaceId === aggregate.app.spaceId);
|
|
1164
|
+
if (!appResolution) throw new InternalError("Aggregate planner returned no plan for the app space — the planner is supposed to always emit one.");
|
|
1165
|
+
const appPlan = appResolution.entry.plan;
|
|
1166
|
+
if (mode === "plan") {
|
|
1167
|
+
const aggregateOps = orderedResolutions.flatMap((r) => r.entry.displayOps);
|
|
1168
|
+
const preview = hasOperationPreview(familyInstance) ? familyInstance.toOperationPreview(aggregateOps) : void 0;
|
|
1169
|
+
const perSpace = buildPerSpaceBreakdown(orderedResolutions, aggregate.app.spaceId, { includeMarkers: false });
|
|
1170
|
+
const summary = `Planned ${aggregateOps.length} operation(s) across ${orderedResolutions.length} space(s)`;
|
|
1171
|
+
return wrapPlanResult({
|
|
1172
|
+
operations: aggregateOps,
|
|
1173
|
+
destination: appPlan.destination,
|
|
1174
|
+
preview,
|
|
1175
|
+
perSpace,
|
|
1176
|
+
summary,
|
|
1177
|
+
...ifDefined("warnings", plannerWarnings)
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
if (options.consentedPlanHash !== void 0) {
|
|
1181
|
+
const planHash = computePlanHash({
|
|
1182
|
+
operations: stripOperations(orderedResolutions.flatMap((r) => r.entry.displayOps)),
|
|
1183
|
+
destination: appPlan.destination
|
|
1184
|
+
});
|
|
1185
|
+
if (planHash !== options.consentedPlanHash) return notOk({
|
|
1186
|
+
code: "CONSENT_PLAN_MISMATCH",
|
|
1187
|
+
summary: "The plan changed between consent and apply",
|
|
1188
|
+
why: "The plan recomputed for the consented apply is not the plan that was consented to, so applying it could destroy something nobody agreed to.",
|
|
1189
|
+
conflicts: void 0,
|
|
1190
|
+
meta: void 0,
|
|
1191
|
+
consentPlanMismatch: {
|
|
1192
|
+
consentedPlanHash: options.consentedPlanHash,
|
|
1193
|
+
planHash
|
|
1194
|
+
},
|
|
1195
|
+
...ifDefined("warnings", plannerWarnings)
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
const applied = await runMigration({
|
|
1199
|
+
aggregate,
|
|
1200
|
+
perSpacePlans: planResult.value.perSpace,
|
|
1201
|
+
applyOrder: planResult.value.applyOrder,
|
|
1202
|
+
driver,
|
|
1203
|
+
familyInstance,
|
|
1204
|
+
migrations,
|
|
1205
|
+
frameworkComponents,
|
|
1206
|
+
policy,
|
|
1207
|
+
action,
|
|
1208
|
+
...ifDefined("onProgress", onProgress)
|
|
1209
|
+
});
|
|
1210
|
+
if (!applied.ok) return buildRunnerFailure({
|
|
1211
|
+
summary: applied.failure.summary,
|
|
1212
|
+
...ifDefined("why", applied.failure.why),
|
|
1213
|
+
meta: applied.failure.meta,
|
|
1214
|
+
...ifDefined("warnings", plannerWarnings),
|
|
1215
|
+
...ifDefined("cause", applied.failure.cause)
|
|
1216
|
+
});
|
|
1217
|
+
const aggregateOps = applied.value.orderedResolutions.flatMap((r) => r.entry.displayOps);
|
|
1218
|
+
const summary = action === "dbInit" ? `Applied ${applied.value.totalOpsExecuted} operation(s) across ${applied.value.orderedResolutions.length} space(s), database signed` : applied.value.totalOpsExecuted === 0 ? `Database already matches contract across ${applied.value.orderedResolutions.length} space(s), signature updated` : `Applied ${applied.value.totalOpsExecuted} operation(s) across ${applied.value.orderedResolutions.length} space(s), signature updated`;
|
|
1219
|
+
return wrapApplyResult({
|
|
1220
|
+
operations: aggregateOps,
|
|
1221
|
+
destination: appPlan.destination,
|
|
1222
|
+
operationsPlanned: applied.value.totalOpsPlanned,
|
|
1223
|
+
operationsExecuted: applied.value.totalOpsExecuted,
|
|
1224
|
+
perSpace: applied.value.perSpace,
|
|
1225
|
+
summary,
|
|
1226
|
+
...ifDefined("warnings", plannerWarnings)
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
function aggregatePlannerWarnings(orderedResolutions) {
|
|
1230
|
+
const warnings = orderedResolutions.flatMap((r) => r.entry.warnings ?? []);
|
|
1231
|
+
return warnings.length > 0 ? warnings : void 0;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Compare the live `_prisma_marker` rows against the aggregate's
|
|
1235
|
+
* declared contract spaces. Any marker row whose `space` is not a space of
|
|
1236
|
+
* the aggregate is an "orphan" — typically a marker left behind by
|
|
1237
|
+
* an extension that was removed from `extensions` without first
|
|
1238
|
+
* cleaning up its on-disk migrations / database tables.
|
|
1239
|
+
*
|
|
1240
|
+
* Returns a {@link CliStructuredError} envelope (code
|
|
1241
|
+
* `MIGRATION.CONTRACT_SPACE_VIOLATION`, `kind: 'orphanMarker'`) for the
|
|
1242
|
+
* first orphan it finds, or `null`
|
|
1243
|
+
* when every marker row maps to a declared contract space. Mirrors the M2
|
|
1244
|
+
* `runContractSpaceVerifierMarkerCheck` envelope so downstream
|
|
1245
|
+
* tooling (integration tests, JSON consumers) keeps asserting on the
|
|
1246
|
+
* same shape.
|
|
1247
|
+
*/
|
|
1248
|
+
function detectOrphanMarkers(aggregate, markerRows) {
|
|
1249
|
+
const aggregateSpaceIds = /* @__PURE__ */ new Set([aggregate.app.spaceId, ...aggregate.extensions.map((m) => m.spaceId)]);
|
|
1250
|
+
const orphans = [];
|
|
1251
|
+
for (const [spaceId, row] of markerRows) if (row !== null && row !== void 0 && !aggregateSpaceIds.has(spaceId)) orphans.push(spaceId);
|
|
1252
|
+
if (orphans.length === 0) return null;
|
|
1253
|
+
orphans.sort((a, b) => a.localeCompare(b));
|
|
1254
|
+
return new CliStructuredError$1("MIGRATION.CONTRACT_SPACE_VIOLATION", orphans.length === 1 ? `Orphan contract-space marker detected for "${orphans[0]}"` : `Orphan contract-space markers detected for ${orphans.length} spaces`, {
|
|
1255
|
+
why: `The database has \`_prisma_marker\` rows for spaces (${orphans.map((s) => `"${s}"`).join(", ")}) that are not declared in the project's \`extensions\`. The aggregate pipeline refuses to advance markers it cannot account for.`,
|
|
1256
|
+
fix: "Either re-declare the missing extension(s) in `extensions` (so the aggregate owns them again), or remove the orphan marker row(s) from `_prisma_marker` once you have confirmed the corresponding tables can be safely retired.",
|
|
1257
|
+
docsUrl: "https://pris.ly/contract-spaces",
|
|
1258
|
+
meta: { violations: orphans.map((spaceId) => ({
|
|
1259
|
+
kind: "orphanMarker",
|
|
1260
|
+
spaceId
|
|
1261
|
+
})) }
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
function mapPlannerError(error) {
|
|
1265
|
+
if (error.kind === "planFromDiffFailed") return blindCast(notOk({
|
|
1266
|
+
code: "PLANNING_FAILED",
|
|
1267
|
+
summary: "Migration planning failed due to conflicts",
|
|
1268
|
+
conflicts: error.conflicts,
|
|
1269
|
+
why: void 0,
|
|
1270
|
+
meta: void 0
|
|
1271
|
+
}));
|
|
1272
|
+
if (error.kind === "extensionPathUnreachable") return buildRunnerFailure({
|
|
1273
|
+
summary: `Cannot resolve apply path for extension space "${error.spaceId}"`,
|
|
1274
|
+
why: `No path in the on-disk migration graph for extension space "${error.spaceId}" reaches the on-disk head ref hash "${error.target}".`,
|
|
1275
|
+
meta: {
|
|
1276
|
+
spaceId: error.spaceId,
|
|
1277
|
+
target: error.target
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
if (error.kind === "extensionPathUnsatisfiable") return buildRunnerFailure({
|
|
1281
|
+
summary: `Cannot resolve apply path for extension space "${error.spaceId}"`,
|
|
1282
|
+
why: `On-disk migration graph for extension space "${error.spaceId}" reaches the on-disk head ref but does not cover required invariants: ${error.missingInvariants.join(", ")}.`,
|
|
1283
|
+
meta: {
|
|
1284
|
+
spaceId: error.spaceId,
|
|
1285
|
+
missingInvariants: error.missingInvariants
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
return buildRunnerFailure({
|
|
1289
|
+
summary: `Aggregate planner policy conflict for space "${error.spaceId}"`,
|
|
1290
|
+
why: error.detail,
|
|
1291
|
+
meta: { spaceId: error.spaceId }
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
function wrapPlanResult(args) {
|
|
1295
|
+
return ok({
|
|
1296
|
+
mode: "plan",
|
|
1297
|
+
plan: {
|
|
1298
|
+
operations: stripOperations(args.operations),
|
|
1299
|
+
...ifDefined("preview", args.preview)
|
|
1300
|
+
},
|
|
1301
|
+
destination: {
|
|
1302
|
+
storageHash: args.destination.storageHash,
|
|
1303
|
+
...ifDefined("profileHash", args.destination.profileHash)
|
|
1304
|
+
},
|
|
1305
|
+
perSpace: args.perSpace,
|
|
1306
|
+
summary: args.summary,
|
|
1307
|
+
...ifDefined("warnings", args.warnings)
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
function wrapApplyResult(args) {
|
|
1311
|
+
return ok({
|
|
1312
|
+
mode: "apply",
|
|
1313
|
+
plan: { operations: stripOperations(args.operations) },
|
|
1314
|
+
destination: {
|
|
1315
|
+
storageHash: args.destination.storageHash,
|
|
1316
|
+
...ifDefined("profileHash", args.destination.profileHash)
|
|
1317
|
+
},
|
|
1318
|
+
execution: {
|
|
1319
|
+
operationsPlanned: args.operationsPlanned,
|
|
1320
|
+
operationsExecuted: args.operationsExecuted
|
|
1321
|
+
},
|
|
1322
|
+
marker: args.destination.profileHash ? {
|
|
1323
|
+
storageHash: args.destination.storageHash,
|
|
1324
|
+
profileHash: args.destination.profileHash
|
|
1325
|
+
} : { storageHash: args.destination.storageHash },
|
|
1326
|
+
perSpace: args.perSpace,
|
|
1327
|
+
summary: args.summary,
|
|
1328
|
+
...ifDefined("warnings", args.warnings)
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
function buildRunnerFailure(args) {
|
|
1332
|
+
return blindCast(notOk({
|
|
1333
|
+
code: "RUNNER_FAILED",
|
|
1334
|
+
summary: args.summary,
|
|
1335
|
+
why: args.why,
|
|
1336
|
+
meta: args.meta,
|
|
1337
|
+
conflicts: void 0,
|
|
1338
|
+
...ifDefined("warnings", args.warnings),
|
|
1339
|
+
...ifDefined("cause", args.cause)
|
|
1340
|
+
}));
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Execute `db init` against the configured contract.
|
|
1344
|
+
*
|
|
1345
|
+
* Routes through the loader → planner → runner pipeline (sub-spec
|
|
1346
|
+
* "Commit-by-commit § Commit 4"). Always additive-only; destructive
|
|
1347
|
+
* changes belong to `db update`.
|
|
1348
|
+
*/
|
|
1349
|
+
async function executeDbInit(options) {
|
|
1350
|
+
return await executeRun({
|
|
1351
|
+
driver: options.driver,
|
|
1352
|
+
adapter: options.adapter,
|
|
1353
|
+
familyInstance: options.familyInstance,
|
|
1354
|
+
contract: options.contract,
|
|
1355
|
+
mode: options.mode,
|
|
1356
|
+
migrations: options.migrations,
|
|
1357
|
+
frameworkComponents: options.frameworkComponents,
|
|
1358
|
+
migrationsDir: options.migrationsDir,
|
|
1359
|
+
targetId: options.targetId,
|
|
1360
|
+
extensions: options.extensions ?? [],
|
|
1361
|
+
policy: { allowedOperationClasses: ["additive"] },
|
|
1362
|
+
action: "dbInit",
|
|
1363
|
+
...ifDefined("onProgress", options.onProgress)
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
const DB_UPDATE_POLICY = { allowedOperationClasses: [
|
|
1367
|
+
"additive",
|
|
1368
|
+
"widening",
|
|
1369
|
+
"destructive"
|
|
1370
|
+
] };
|
|
1371
|
+
/**
|
|
1372
|
+
* Execute `db update` against the configured contract.
|
|
1373
|
+
*
|
|
1374
|
+
* Routes through the loader → planner → runner pipeline. Destructive
|
|
1375
|
+
* operations require either `acceptDataLoss: true` or a prior
|
|
1376
|
+
* `mode: 'plan'` invocation that surfaces the destructive ops; the
|
|
1377
|
+
* confirmation gate is implemented here so the lower-level applier
|
|
1378
|
+
* remains policy-agnostic.
|
|
1379
|
+
*/
|
|
1380
|
+
async function executeDbUpdate(options) {
|
|
1381
|
+
const sharedInputs = {
|
|
1382
|
+
driver: options.driver,
|
|
1383
|
+
adapter: options.adapter,
|
|
1384
|
+
familyInstance: options.familyInstance,
|
|
1385
|
+
contract: options.contract,
|
|
1386
|
+
migrations: options.migrations,
|
|
1387
|
+
frameworkComponents: options.frameworkComponents,
|
|
1388
|
+
migrationsDir: options.migrationsDir,
|
|
1389
|
+
targetId: options.targetId,
|
|
1390
|
+
extensions: options.extensions ?? [],
|
|
1391
|
+
policy: DB_UPDATE_POLICY,
|
|
1392
|
+
action: "dbUpdate",
|
|
1393
|
+
...ifDefined("onProgress", options.onProgress)
|
|
1394
|
+
};
|
|
1395
|
+
if (options.mode === "apply" && !options.acceptDataLoss && options.consent === void 0) {
|
|
1396
|
+
const refusal = await guardDestructiveChanges(sharedInputs);
|
|
1397
|
+
if (refusal !== null) return refusal;
|
|
1398
|
+
}
|
|
1399
|
+
return await executeRun({
|
|
1400
|
+
...sharedInputs,
|
|
1401
|
+
mode: options.mode,
|
|
1402
|
+
...ifDefined("consentedPlanHash", options.consent?.planHash)
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Pre-plan once when running `db update apply` without `acceptDataLoss`.
|
|
1407
|
+
* Surfaces destructive operations across every space; if any are
|
|
1408
|
+
* planned, returns a `DESTRUCTIVE_CHANGES` failure that the CLI shows
|
|
1409
|
+
* as a confirmation prompt. Returns `null` when the apply is safe to
|
|
1410
|
+
* run.
|
|
1411
|
+
*/
|
|
1412
|
+
async function guardDestructiveChanges(sharedInputs) {
|
|
1413
|
+
const planResult = await executeRun({
|
|
1414
|
+
...sharedInputs,
|
|
1415
|
+
mode: "plan"
|
|
1416
|
+
});
|
|
1417
|
+
if (!planResult.ok) return planResult;
|
|
1418
|
+
const destructiveOps = planResult.value.plan.operations.filter((op) => op.operationClass === "destructive").map((op) => ({
|
|
1419
|
+
id: op.id,
|
|
1420
|
+
label: op.label
|
|
1421
|
+
}));
|
|
1422
|
+
if (destructiveOps.length === 0) return null;
|
|
1423
|
+
const databaseName = await sharedInputs.driver.databaseName?.();
|
|
1424
|
+
return notOk({
|
|
1425
|
+
code: "DESTRUCTIVE_CHANGES",
|
|
1426
|
+
summary: `Planned ${destructiveOps.length} destructive operation(s) that require confirmation`,
|
|
1427
|
+
why: "Destructive operations require explicit consent, which the caller has not given",
|
|
1428
|
+
conflicts: void 0,
|
|
1429
|
+
meta: void 0,
|
|
1430
|
+
destructiveChanges: {
|
|
1431
|
+
destructiveOperations: destructiveOps,
|
|
1432
|
+
databaseName,
|
|
1433
|
+
planHash: computePlanHash({
|
|
1434
|
+
operations: planResult.value.plan.operations,
|
|
1435
|
+
destination: planResult.value.destination
|
|
1436
|
+
})
|
|
1437
|
+
},
|
|
1438
|
+
...ifDefined("warnings", planResult.value.warnings)
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Span IDs emitted via `onProgress` during the aggregate verify flow.
|
|
1443
|
+
* Mirrors the span identifiers used by the legacy precheck / marker-check
|
|
1444
|
+
* helpers so structured-output renderers and progress tests keep working.
|
|
1445
|
+
*/
|
|
1446
|
+
const SPAN_IDS = {
|
|
1447
|
+
introspect: "introspect",
|
|
1448
|
+
verify: "verify"
|
|
1449
|
+
};
|
|
1450
|
+
/**
|
|
1451
|
+
* Loader → verifier pipeline shared by `db verify` modes (`full`,
|
|
1452
|
+
* `marker-only`, `schema-only`).
|
|
1453
|
+
*
|
|
1454
|
+
* 1. **Load**: build a {@link import('@internal/migration-tools/aggregate').ContractSpaceAggregate}
|
|
1455
|
+
* from descriptors + on-disk on-disk artefacts. Layout / drift /
|
|
1456
|
+
* integrity / disjointness violations short-circuit with a
|
|
1457
|
+
* structured CLI error.
|
|
1458
|
+
* 2. **Read DB state**: marker rows + (when `skipSchema` is `false`)
|
|
1459
|
+
* schema introspection.
|
|
1460
|
+
* 3. **Verify**: {@link verifyMigration} returns per-space `markerCheck` +
|
|
1461
|
+
* per-space `schemaCheck` (each contract space verified against the full schema,
|
|
1462
|
+
* then scoped to its own contract space). Marker drift is returned as the
|
|
1463
|
+
* success value's `markerDrift` envelope for callers to settle as a
|
|
1464
|
+
* finding. Verify results are returned to the caller verbatim.
|
|
1465
|
+
*/
|
|
1466
|
+
async function executeDbVerify(options) {
|
|
1467
|
+
const { driver, familyInstance, onProgress, skipSchema, skipMarker } = options;
|
|
1468
|
+
const loaded = await buildContractSpaceAggregate(buildLoadInputs(options));
|
|
1469
|
+
if (!loaded.ok) return notOk(loaded.failure);
|
|
1470
|
+
const aggregate = loaded.value;
|
|
1471
|
+
const markersBySpaceId = await familyInstance.readAllMarkers({ driver });
|
|
1472
|
+
const schemaIntrospection = skipSchema ? null : await runIntrospection({
|
|
1473
|
+
driver,
|
|
1474
|
+
familyInstance,
|
|
1475
|
+
onProgress,
|
|
1476
|
+
contract: collectAggregateNamespaces(aggregate)
|
|
1477
|
+
});
|
|
1478
|
+
const classifySubjectGranularity = hasSchemaSubjectClassifier(familyInstance) ? (issue) => familyInstance.classifySubjectGranularity(issue) : void 0;
|
|
1479
|
+
const classifyEntityKind = hasSchemaSubjectClassifier(familyInstance) ? (issue) => familyInstance.classifyEntityKind(issue) : void 0;
|
|
1480
|
+
emitVerifySpan(onProgress, "spanStart");
|
|
1481
|
+
return finaliseVerifyResult({
|
|
1482
|
+
verifyResult: verifyMigration({
|
|
1483
|
+
aggregate,
|
|
1484
|
+
markersBySpaceId,
|
|
1485
|
+
schemaIntrospection,
|
|
1486
|
+
mode: options.mode,
|
|
1487
|
+
verifySchemaForSpace: createPerSpaceVerifier(options),
|
|
1488
|
+
...ifDefined("classifySubjectGranularity", classifySubjectGranularity),
|
|
1489
|
+
...ifDefined("classifyEntityKind", classifyEntityKind)
|
|
1490
|
+
}),
|
|
1491
|
+
aggregate,
|
|
1492
|
+
skipMarker,
|
|
1493
|
+
onProgress
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
function buildLoadInputs(options) {
|
|
1497
|
+
return {
|
|
1498
|
+
targetId: options.targetId,
|
|
1499
|
+
migrationsDir: options.migrationsDir,
|
|
1500
|
+
appContract: options.contract,
|
|
1501
|
+
extensions: options.extensions,
|
|
1502
|
+
deserializeContract: (json) => options.familyInstance.deserializeContract(json)
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
async function runIntrospection(args) {
|
|
1506
|
+
const { driver, familyInstance, onProgress, contract } = args;
|
|
1507
|
+
onProgress?.({
|
|
1508
|
+
action: "dbVerify",
|
|
1509
|
+
kind: "spanStart",
|
|
1510
|
+
spanId: SPAN_IDS.introspect,
|
|
1511
|
+
label: "Introspecting database schema"
|
|
1512
|
+
});
|
|
1513
|
+
try {
|
|
1514
|
+
const result = await familyInstance.introspect({
|
|
1515
|
+
driver,
|
|
1516
|
+
contract
|
|
1517
|
+
});
|
|
1518
|
+
onProgress?.({
|
|
1519
|
+
action: "dbVerify",
|
|
1520
|
+
kind: "spanEnd",
|
|
1521
|
+
spanId: SPAN_IDS.introspect,
|
|
1522
|
+
outcome: "ok"
|
|
1523
|
+
});
|
|
1524
|
+
return result;
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
onProgress?.({
|
|
1527
|
+
action: "dbVerify",
|
|
1528
|
+
kind: "spanEnd",
|
|
1529
|
+
spanId: SPAN_IDS.introspect,
|
|
1530
|
+
outcome: "error"
|
|
1531
|
+
});
|
|
1532
|
+
throw error;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Build the per-space schema callback handed to the aggregate verifier.
|
|
1537
|
+
* When `skipSchema` is true the callback short-circuits with a synthetic
|
|
1538
|
+
* `ok` result so the verifier still runs the (cheap) schemaCheck loop
|
|
1539
|
+
* without invoking the family's verification path.
|
|
1540
|
+
*/
|
|
1541
|
+
function createPerSpaceVerifier(options) {
|
|
1542
|
+
const { skipSchema, familyInstance, frameworkComponents } = options;
|
|
1543
|
+
return (schema, space, verifyMode) => {
|
|
1544
|
+
if (skipSchema) return buildSkippedSchemaResult(space);
|
|
1545
|
+
return familyInstance.verifySchema({
|
|
1546
|
+
contract: space.contract(),
|
|
1547
|
+
schema,
|
|
1548
|
+
strict: verifyMode === "strict",
|
|
1549
|
+
frameworkComponents
|
|
1550
|
+
});
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
function emitVerifySpan(onProgress, kind) {
|
|
1554
|
+
if (kind === "spanStart") {
|
|
1555
|
+
onProgress?.({
|
|
1556
|
+
action: "dbVerify",
|
|
1557
|
+
kind: "spanStart",
|
|
1558
|
+
spanId: SPAN_IDS.verify,
|
|
1559
|
+
label: "Verifying contract spaces"
|
|
1560
|
+
});
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
onProgress?.({
|
|
1564
|
+
action: "dbVerify",
|
|
1565
|
+
kind: "spanEnd",
|
|
1566
|
+
spanId: SPAN_IDS.verify,
|
|
1567
|
+
outcome: kind === "spanEndOk" ? "ok" : "error"
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Map an {@link VerifierOutput} to the operation's
|
|
1572
|
+
* {@link ExecuteDbVerifyResult}, applying the `skipMarker` policy used
|
|
1573
|
+
* by the CLI's `--schema-only` mode.
|
|
1574
|
+
*/
|
|
1575
|
+
function finaliseVerifyResult(args) {
|
|
1576
|
+
const { verifyResult, aggregate, skipMarker, onProgress } = args;
|
|
1577
|
+
if (!verifyResult.ok) {
|
|
1578
|
+
emitVerifySpan(onProgress, "spanEndError");
|
|
1579
|
+
return notOk(new CliStructuredError$1("MIGRATION.CONTRACT_SPACE_VIOLATION", "Aggregate verifier introspection failed", {
|
|
1580
|
+
why: verifyResult.failure.detail,
|
|
1581
|
+
fix: "Check database connectivity and the introspection tooling.",
|
|
1582
|
+
docsUrl: "https://pris.ly/contract-spaces"
|
|
1583
|
+
}));
|
|
1584
|
+
}
|
|
1585
|
+
const markerDrift = skipMarker ? null : mapMarkerCheckFailures(aggregate.app.spaceId, verifyResult.value.markerCheck);
|
|
1586
|
+
emitVerifySpan(onProgress, markerDrift === null ? "spanEndOk" : "spanEndError");
|
|
1587
|
+
return ok({
|
|
1588
|
+
schemaResults: verifyResult.value.schemaCheck.perSpace,
|
|
1589
|
+
unclaimed: verifyResult.value.schemaCheck.unclaimed,
|
|
1590
|
+
spaceOrder: [aggregate.app.spaceId, ...aggregate.extensions.map((e) => e.spaceId)],
|
|
1591
|
+
appSpaceId: aggregate.app.spaceId,
|
|
1592
|
+
markerDrift
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
function buildSkippedSchemaResult(space) {
|
|
1596
|
+
const contract = space.contract();
|
|
1597
|
+
const headRef = requireHeadRef(space);
|
|
1598
|
+
const profileHash = castAs(contract).profileHash;
|
|
1599
|
+
return {
|
|
1600
|
+
ok: true,
|
|
1601
|
+
summary: "Schema verification skipped",
|
|
1602
|
+
contract: {
|
|
1603
|
+
storageHash: headRef.hash,
|
|
1604
|
+
...profileHash ? { profileHash } : {}
|
|
1605
|
+
},
|
|
1606
|
+
target: { expected: contract.target },
|
|
1607
|
+
schema: { issues: [] },
|
|
1608
|
+
timings: { total: 0 }
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Translate per-space marker check failures and orphan markers into a
|
|
1613
|
+
* single CLI structured error envelope. Preserves the legacy code
|
|
1614
|
+
* `MIGRATION.CONTRACT_SPACE_VIOLATION` (was emitted by
|
|
1615
|
+
* `runContractSpaceVerifierMarkerCheck`).
|
|
1616
|
+
*/
|
|
1617
|
+
function mapMarkerCheckFailures(appSpaceId, section) {
|
|
1618
|
+
const violations = [];
|
|
1619
|
+
for (const [spaceId, result] of section.perSpace) {
|
|
1620
|
+
if (result.kind === "ok" || result.kind === "absent") continue;
|
|
1621
|
+
if (result.kind === "hashMismatch") {
|
|
1622
|
+
violations.push({
|
|
1623
|
+
kind: "hashMismatch",
|
|
1624
|
+
spaceId,
|
|
1625
|
+
remediation: spaceId === appSpaceId ? "Run `prisma-cli db update` to advance the marker, or roll the database back to the recorded hash." : `Apply on-disk migrations under \`migrations/${spaceId}/\` to advance the marker, or remove the conflicting marker row.`
|
|
1626
|
+
});
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
if (result.kind === "missingInvariants") violations.push({
|
|
1630
|
+
kind: "invariantsMismatch",
|
|
1631
|
+
spaceId,
|
|
1632
|
+
remediation: `Re-apply the migrations under \`migrations/${spaceId}/\` so the marker carries invariants: ${result.missing.join(", ")}.`
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
for (const orphan of section.orphanMarkers) violations.push({
|
|
1636
|
+
kind: "orphanMarker",
|
|
1637
|
+
spaceId: orphan.spaceId,
|
|
1638
|
+
remediation: `Add the corresponding extension to \`extensions\` in \`prisma.config.ts\`, or delete the orphan marker row for "${orphan.spaceId}".`
|
|
1639
|
+
});
|
|
1640
|
+
if (violations.length === 0) return null;
|
|
1641
|
+
const lines = violations.map((v) => `- [${v.kind}] ${v.spaceId}: ${v.remediation}`);
|
|
1642
|
+
return new CliStructuredError$1("MIGRATION.CONTRACT_SPACE_VIOLATION", violations.length === 1 ? "Contract-space verifier found a violation" : `Contract-space verifier found violations (${violations.length})`, {
|
|
1643
|
+
why: `The on-disk \`migrations/\` directory, the \`extensions\` declaration, and the live database marker rows are not in agreement.\n${lines.join("\n")}`,
|
|
1644
|
+
fix: violations[0]?.remediation ?? "Review and reconcile the violations listed above.",
|
|
1645
|
+
docsUrl: "https://pris.ly/contract-spaces",
|
|
1646
|
+
meta: { violations }
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Apply pending migrations across every contract space (app +
|
|
1651
|
+
* extensions). Replay-only: graph-walk against the on-disk graph for
|
|
1652
|
+
* every contract space; no synth, no introspection.
|
|
1653
|
+
*
|
|
1654
|
+
* Pipeline:
|
|
1655
|
+
*
|
|
1656
|
+
* 1. Load aggregate from disk (loader hydrates extension graphs;
|
|
1657
|
+
* caller provides app-space packages).
|
|
1658
|
+
* 2. Read live marker rows per space (`familyInstance.readAllMarkers`).
|
|
1659
|
+
* 3. Per space: `resolveRecordedPath` plots the path from the live
|
|
1660
|
+
* marker to `space.headRef.hash` (or `refHash` for the app
|
|
1661
|
+
* space when provided). An empty-graph space whose elements are ALL
|
|
1662
|
+
* externally managed resolves declaratively (marker to head, zero
|
|
1663
|
+
* ops), mirroring the db-init aggregate planner: such a space ships
|
|
1664
|
+
* no DDL and has nothing to author. Every other empty-graph space
|
|
1665
|
+
* fails loudly — "never planned" is a user-error condition for
|
|
1666
|
+
* replay.
|
|
1667
|
+
* 4. Hand off to {@link runMigration} (the runner-driving tail
|
|
1668
|
+
* shared with `db init` / `db update`). Marker advancement is
|
|
1669
|
+
* inside the per-space transaction.
|
|
1670
|
+
*
|
|
1671
|
+
* Encodes the replay-only contract: the app contract space must have an
|
|
1672
|
+
* authored migration graph on disk before this operation can advance it.
|
|
1673
|
+
*/
|
|
1674
|
+
async function executeMigrate(options) {
|
|
1675
|
+
const { driver, familyInstance, contract, migrations, frameworkComponents, migrationsDir, extensions, targetId, refHash, refInvariants, refName, onProgress } = options;
|
|
1676
|
+
const loaded = await buildContractSpaceAggregate({
|
|
1677
|
+
targetId,
|
|
1678
|
+
migrationsDir,
|
|
1679
|
+
appContract: contract,
|
|
1680
|
+
extensions,
|
|
1681
|
+
deserializeContract: (json) => familyInstance.deserializeContract(json)
|
|
1682
|
+
});
|
|
1683
|
+
if (!loaded.ok) throw loaded.failure;
|
|
1684
|
+
const aggregate = loaded.value;
|
|
1685
|
+
const markerRows = await familyInstance.readAllMarkers({ driver });
|
|
1686
|
+
const allSpaces = [aggregate.app, ...aggregate.extensions];
|
|
1687
|
+
const perSpacePlans = /* @__PURE__ */ new Map();
|
|
1688
|
+
const atHeadResolutions = /* @__PURE__ */ new Map();
|
|
1689
|
+
for (const space of allSpaces) {
|
|
1690
|
+
const isAppSpace = space.spaceId === aggregate.app.spaceId;
|
|
1691
|
+
const headRef = requireHeadRef(space);
|
|
1692
|
+
const spaceTargetHash = isAppSpace && refHash !== void 0 ? refHash : headRef.hash;
|
|
1693
|
+
const outcome = planSpacePath({
|
|
1694
|
+
space,
|
|
1695
|
+
aggregate,
|
|
1696
|
+
targetHash: spaceTargetHash,
|
|
1697
|
+
refInvariants: isAppSpace && refHash !== void 0 ? refInvariants : void 0,
|
|
1698
|
+
liveMarker: markerRows.get(space.spaceId) ?? null,
|
|
1699
|
+
...isAppSpace ? { refName } : {}
|
|
1700
|
+
});
|
|
1701
|
+
if (outcome.kind === "at-head") {
|
|
1702
|
+
atHeadResolutions.set(space.spaceId, outcome.plan);
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
if (outcome.kind === "never-planned") return notOk(buildNeverPlannedFailure(outcome.spaceId, outcome.targetHash));
|
|
1706
|
+
if (outcome.kind === "unreachable") return notOk(buildPathNotFoundFailure(outcome.spaceId, outcome.liveMarker, outcome.targetHash));
|
|
1707
|
+
if (outcome.kind === "unsatisfiable") {
|
|
1708
|
+
const structural = findPathWithDecision(outcome.targetSpace.graph(), outcome.liveHash, spaceTargetHash, { required: /* @__PURE__ */ new Set() });
|
|
1709
|
+
const structuralPath = structural.kind === "ok" ? structural.decision.selectedPath.map((edge) => ({
|
|
1710
|
+
dirName: edge.dirName,
|
|
1711
|
+
migrationHash: edge.migrationHash,
|
|
1712
|
+
from: edge.from,
|
|
1713
|
+
to: edge.to,
|
|
1714
|
+
invariants: edge.invariants
|
|
1715
|
+
})) : [];
|
|
1716
|
+
throw errorNoInvariantPath({
|
|
1717
|
+
...outcome.refName !== void 0 ? { refName: outcome.refName } : {},
|
|
1718
|
+
required: outcome.targetInvariants,
|
|
1719
|
+
missing: outcome.missing,
|
|
1720
|
+
structuralPath
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
perSpacePlans.set(space.spaceId, outcome.plan);
|
|
1724
|
+
}
|
|
1725
|
+
const canonicalOrder = [...aggregate.extensions.map((m) => m.spaceId), aggregate.app.spaceId];
|
|
1726
|
+
const applyOrder = canonicalOrder.filter((spaceId) => perSpacePlans.has(spaceId));
|
|
1727
|
+
if (!applyOrder.some((spaceId) => {
|
|
1728
|
+
const entry = perSpacePlans.get(spaceId);
|
|
1729
|
+
return entry !== void 0 && planRequiresExecution(entry);
|
|
1730
|
+
})) {
|
|
1731
|
+
const ordered = canonicalOrder.filter((spaceId) => perSpacePlans.has(spaceId) || atHeadResolutions.has(spaceId)).map((spaceId) => {
|
|
1732
|
+
const entry = perSpacePlans.get(spaceId) ?? atHeadResolutions.get(spaceId);
|
|
1733
|
+
if (entry === void 0) throw new InternalError(`Unreachable: missing per-space plan for "${spaceId}"`);
|
|
1734
|
+
return {
|
|
1735
|
+
spaceId,
|
|
1736
|
+
entry
|
|
1737
|
+
};
|
|
1738
|
+
});
|
|
1739
|
+
const perSpace = buildPerSpaceBreakdown(ordered, aggregate.app.spaceId, { includeMarkers: true });
|
|
1740
|
+
const totalSpaces = ordered.length;
|
|
1741
|
+
return ok(buildSuccess({
|
|
1742
|
+
aggregate,
|
|
1743
|
+
orderedResolutions: ordered,
|
|
1744
|
+
perSpace,
|
|
1745
|
+
totalOpsExecuted: 0,
|
|
1746
|
+
summary: totalSpaces === 0 ? "Already up to date — no contract spaces are loaded" : totalSpaces === 1 ? "Already up to date" : `Already up to date across ${totalSpaces} space(s)`
|
|
1747
|
+
}));
|
|
1748
|
+
}
|
|
1749
|
+
const applied = await runMigration({
|
|
1750
|
+
aggregate,
|
|
1751
|
+
perSpacePlans,
|
|
1752
|
+
applyOrder,
|
|
1753
|
+
driver,
|
|
1754
|
+
familyInstance,
|
|
1755
|
+
migrations,
|
|
1756
|
+
frameworkComponents,
|
|
1757
|
+
policy: { allowedOperationClasses: [
|
|
1758
|
+
"additive",
|
|
1759
|
+
"widening",
|
|
1760
|
+
"destructive",
|
|
1761
|
+
"data"
|
|
1762
|
+
] },
|
|
1763
|
+
action: "migrate",
|
|
1764
|
+
...ifDefined("onProgress", onProgress)
|
|
1765
|
+
});
|
|
1766
|
+
if (!applied.ok) return notOk({
|
|
1767
|
+
code: "RUNNER_FAILED",
|
|
1768
|
+
summary: applied.failure.summary,
|
|
1769
|
+
why: applied.failure.why,
|
|
1770
|
+
meta: applied.failure.meta,
|
|
1771
|
+
...ifDefined("cause", applied.failure.cause)
|
|
1772
|
+
});
|
|
1773
|
+
const orderedAll = canonicalOrder.filter((spaceId) => perSpacePlans.has(spaceId) || atHeadResolutions.has(spaceId)).map((spaceId) => {
|
|
1774
|
+
if (perSpacePlans.has(spaceId)) {
|
|
1775
|
+
const fromRunner = applied.value.orderedResolutions.find((r) => r.spaceId === spaceId);
|
|
1776
|
+
if (fromRunner !== void 0) return fromRunner;
|
|
1777
|
+
}
|
|
1778
|
+
const entry = atHeadResolutions.get(spaceId);
|
|
1779
|
+
if (entry === void 0) throw new InternalError(`Unreachable: missing per-space plan for "${spaceId}"`);
|
|
1780
|
+
return {
|
|
1781
|
+
spaceId,
|
|
1782
|
+
entry
|
|
1783
|
+
};
|
|
1784
|
+
});
|
|
1785
|
+
const perSpaceAll = buildPerSpaceBreakdown(orderedAll, aggregate.app.spaceId, { includeMarkers: true });
|
|
1786
|
+
const summary = `Applied ${applied.value.orderedResolutions.reduce((sum, r) => sum + r.entry.migrationEdges.length, 0)} migration(s) (${applied.value.totalOpsExecuted} operation(s)) across ${orderedAll.length} contract space(s)`;
|
|
1787
|
+
return ok(buildSuccess({
|
|
1788
|
+
aggregate,
|
|
1789
|
+
orderedResolutions: orderedAll,
|
|
1790
|
+
perSpace: perSpaceAll,
|
|
1791
|
+
totalOpsExecuted: applied.value.totalOpsExecuted,
|
|
1792
|
+
summary
|
|
1793
|
+
}));
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Compute the graph-walk path for one contract space.
|
|
1797
|
+
*
|
|
1798
|
+
* Encapsulates the invariant-correct input assembly that both
|
|
1799
|
+
* `executeMigrate` and `executeMigrateShowCommand` must use:
|
|
1800
|
+
* - `currentMarker` carries the full live marker including `invariants`
|
|
1801
|
+
* (not a stripped `{ storageHash, invariants: [] }` shell).
|
|
1802
|
+
* - `targetInvariants` uses the caller-supplied `refInvariants` when a
|
|
1803
|
+
* `--to` ref was resolved (not always the file head ref's invariants).
|
|
1804
|
+
*
|
|
1805
|
+
* Both callers map the returned `SpacePathOutcome` to their own error
|
|
1806
|
+
* representation; the path-compute logic is shared and identical.
|
|
1807
|
+
*
|
|
1808
|
+
* @internal Exported for `executeMigrateShowCommand`.
|
|
1809
|
+
*/
|
|
1810
|
+
function planSpacePath({ space, aggregate, targetHash, refInvariants, liveMarker, refName }) {
|
|
1811
|
+
const isAppSpace = space.spaceId === aggregate.app.spaceId;
|
|
1812
|
+
const headRef = requireHeadRef(space);
|
|
1813
|
+
if (space.graph().nodes.size === 0) {
|
|
1814
|
+
const liveHash = liveMarker?.storageHash;
|
|
1815
|
+
if (targetHash === liveHash || liveHash === void 0 && targetHash === "empty") return {
|
|
1816
|
+
kind: "at-head",
|
|
1817
|
+
plan: buildAtHeadResolution({
|
|
1818
|
+
aggregateTargetId: aggregate.targetId,
|
|
1819
|
+
space,
|
|
1820
|
+
targetHash,
|
|
1821
|
+
liveMarker
|
|
1822
|
+
})
|
|
1823
|
+
};
|
|
1824
|
+
if (!isAppSpace && allStorageElementsExternal(space.contract())) {
|
|
1825
|
+
if (headRef.invariants.length > 0) return {
|
|
1826
|
+
kind: "unsatisfiable",
|
|
1827
|
+
spaceId: space.spaceId,
|
|
1828
|
+
isAppSpace,
|
|
1829
|
+
missing: [...headRef.invariants].sort(),
|
|
1830
|
+
targetInvariants: headRef.invariants,
|
|
1831
|
+
targetSpace: space,
|
|
1832
|
+
liveHash: liveHash ?? "empty",
|
|
1833
|
+
refName: void 0
|
|
1834
|
+
};
|
|
1835
|
+
return {
|
|
1836
|
+
kind: "ok",
|
|
1837
|
+
plan: buildAtHeadResolution({
|
|
1838
|
+
aggregateTargetId: aggregate.targetId,
|
|
1839
|
+
space,
|
|
1840
|
+
targetHash,
|
|
1841
|
+
liveMarker
|
|
1842
|
+
})
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
return {
|
|
1846
|
+
kind: "never-planned",
|
|
1847
|
+
spaceId: space.spaceId,
|
|
1848
|
+
targetHash
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
const targetInvariants = isAppSpace && refInvariants !== void 0 ? refInvariants : headRef.invariants;
|
|
1852
|
+
const targetSpace = targetHash === headRef.hash && targetInvariants === headRef.invariants ? space : {
|
|
1853
|
+
...space,
|
|
1854
|
+
headRef: {
|
|
1855
|
+
hash: targetHash,
|
|
1856
|
+
invariants: targetInvariants
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
const walked = resolveRecordedPath({
|
|
1860
|
+
aggregateTargetId: aggregate.targetId,
|
|
1861
|
+
space: targetSpace,
|
|
1862
|
+
currentMarker: liveMarker,
|
|
1863
|
+
...isAppSpace && refName !== void 0 ? { refName } : {}
|
|
1864
|
+
});
|
|
1865
|
+
if (walked.kind === "unreachable") return {
|
|
1866
|
+
kind: "unreachable",
|
|
1867
|
+
spaceId: space.spaceId,
|
|
1868
|
+
liveMarker,
|
|
1869
|
+
targetHash
|
|
1870
|
+
};
|
|
1871
|
+
if (walked.kind === "unsatisfiable") {
|
|
1872
|
+
const liveHash = liveMarker?.storageHash ?? "empty";
|
|
1873
|
+
return {
|
|
1874
|
+
kind: "unsatisfiable",
|
|
1875
|
+
spaceId: space.spaceId,
|
|
1876
|
+
isAppSpace,
|
|
1877
|
+
missing: walked.missing,
|
|
1878
|
+
targetInvariants,
|
|
1879
|
+
targetSpace,
|
|
1880
|
+
liveHash,
|
|
1881
|
+
refName
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
return {
|
|
1885
|
+
kind: "ok",
|
|
1886
|
+
plan: walked.result
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Build a zero-op {@link PerSpacePlan} for an empty-graph space —
|
|
1891
|
+
* either one whose live marker already matches the target (at-head), or
|
|
1892
|
+
* an all-external extension space whose marker must advance to the head
|
|
1893
|
+
* ref with no DDL (declared-state). Lets the apply pipeline thread the
|
|
1894
|
+
* space through `perSpacePlans` -> `applyOrder` -> the success
|
|
1895
|
+
* envelope's `perSpace[]` block so the result reflects every loaded
|
|
1896
|
+
* space, even when there is nothing to execute.
|
|
1897
|
+
*/
|
|
1898
|
+
function buildAtHeadResolution(args) {
|
|
1899
|
+
const { aggregateTargetId, space, targetHash, liveMarker } = args;
|
|
1900
|
+
return {
|
|
1901
|
+
plan: {
|
|
1902
|
+
targetId: aggregateTargetId,
|
|
1903
|
+
spaceId: space.spaceId,
|
|
1904
|
+
origin: liveMarker === null ? null : { storageHash: liveMarker.storageHash },
|
|
1905
|
+
destination: { storageHash: targetHash },
|
|
1906
|
+
operations: [],
|
|
1907
|
+
providedInvariants: []
|
|
1908
|
+
},
|
|
1909
|
+
displayOps: [],
|
|
1910
|
+
destinationContract: space.contract(),
|
|
1911
|
+
strategy: "declared-state",
|
|
1912
|
+
migrationEdges: [buildFabricatedMigrationEdge({
|
|
1913
|
+
currentMarkerStorageHash: liveMarker?.storageHash,
|
|
1914
|
+
destinationStorageHash: targetHash,
|
|
1915
|
+
operationCount: 0
|
|
1916
|
+
})]
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* A plan needs the runner when it executes operations or advances the
|
|
1921
|
+
* space's marker (a declared-state resolution has zero operations but a
|
|
1922
|
+
* destination hash the live marker doesn't carry yet).
|
|
1923
|
+
*/
|
|
1924
|
+
function planRequiresExecution(entry) {
|
|
1925
|
+
if (entry.plan.operations.length > 0) return true;
|
|
1926
|
+
return entry.plan.origin?.storageHash !== entry.plan.destination.storageHash;
|
|
1927
|
+
}
|
|
1928
|
+
function buildSuccess(args) {
|
|
1929
|
+
const appResolution = args.orderedResolutions.find((r) => r.spaceId === args.aggregate.app.spaceId);
|
|
1930
|
+
const appMarkerHash = appResolution?.entry.plan.destination.storageHash ?? requireHeadRef(args.aggregate.app).hash;
|
|
1931
|
+
const applied = args.orderedResolutions.flatMap((r) => {
|
|
1932
|
+
return r.entry.migrationEdges.map((edge) => ({
|
|
1933
|
+
spaceId: r.spaceId,
|
|
1934
|
+
dirName: edge.dirName,
|
|
1935
|
+
migrationHash: edge.migrationHash,
|
|
1936
|
+
from: edge.from,
|
|
1937
|
+
to: edge.to,
|
|
1938
|
+
operationsExecuted: edge.operationCount
|
|
1939
|
+
}));
|
|
1940
|
+
});
|
|
1941
|
+
const appPlan = appResolution?.entry;
|
|
1942
|
+
const pathDecision = appPlan?.pathDecision ? {
|
|
1943
|
+
fromHash: appPlan.pathDecision.fromHash,
|
|
1944
|
+
toHash: appPlan.pathDecision.toHash,
|
|
1945
|
+
alternativeCount: appPlan.pathDecision.alternativeCount,
|
|
1946
|
+
tieBreakReasons: appPlan.pathDecision.tieBreakReasons,
|
|
1947
|
+
...appPlan.pathDecision.refName !== void 0 ? { refName: appPlan.pathDecision.refName } : {},
|
|
1948
|
+
requiredInvariants: appPlan.pathDecision.requiredInvariants ?? [],
|
|
1949
|
+
satisfiedInvariants: appPlan.pathDecision.satisfiedInvariants ?? [],
|
|
1950
|
+
selectedPath: appPlan.pathDecision.selectedPath.map((entry) => ({
|
|
1951
|
+
dirName: entry.dirName,
|
|
1952
|
+
migrationHash: entry.migrationHash,
|
|
1953
|
+
from: entry.from,
|
|
1954
|
+
to: entry.to,
|
|
1955
|
+
invariants: entry.invariants
|
|
1956
|
+
}))
|
|
1957
|
+
} : void 0;
|
|
1958
|
+
return {
|
|
1959
|
+
migrationsApplied: applied.length,
|
|
1960
|
+
markerHash: appMarkerHash,
|
|
1961
|
+
applied,
|
|
1962
|
+
summary: args.summary,
|
|
1963
|
+
perSpace: args.perSpace,
|
|
1964
|
+
...pathDecision !== void 0 ? { pathDecision } : {}
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Build the `neverPlanned` failure raised when a contract space that
|
|
1969
|
+
* declares managed storage elements has no on-disk migration graph but
|
|
1970
|
+
* migrate was asked to reach a target hash. All-external spaces never reach
|
|
1971
|
+
* this: they resolve declaratively (marker advances to the head ref with
|
|
1972
|
+
* zero operations). The `why` states only the condition; the recovery
|
|
1973
|
+
* sequence is composed by `errorPathUnreachable`'s `fix`.
|
|
1974
|
+
*
|
|
1975
|
+
* @internal Exported for testing only.
|
|
1976
|
+
*/
|
|
1977
|
+
function buildNeverPlannedFailure(spaceId, targetHash) {
|
|
1978
|
+
return {
|
|
1979
|
+
code: "MIGRATION_PATH_NOT_FOUND",
|
|
1980
|
+
summary: `No on-disk migrations for contract space "${spaceId}"`,
|
|
1981
|
+
why: `migrate is replay-only: a contract space that declares managed storage elements must have an authored migration graph on disk. Space "${spaceId}" has no migrations under \`migrations/${spaceId}/\` but its head ref targets "${targetHash}".`,
|
|
1982
|
+
meta: {
|
|
1983
|
+
spaceId,
|
|
1984
|
+
target: targetHash,
|
|
1985
|
+
kind: "neverPlanned"
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Build the `pathUnreachable` failure raised when an emitted contract has no
|
|
1991
|
+
* on-disk migration edge from the current marker to the target. The `why`
|
|
1992
|
+
* states only the condition (no edge between the two named states, and migrate
|
|
1993
|
+
* replays edges rather than inventing them); the recovery sequence — plan the
|
|
1994
|
+
* edge, then re-apply — is composed by `errorPathUnreachable`'s `fix`, so the
|
|
1995
|
+
* two read as one non-redundant plan-then-apply story.
|
|
1996
|
+
*
|
|
1997
|
+
* @internal Exported for testing only.
|
|
1998
|
+
*/
|
|
1999
|
+
function buildPathNotFoundFailure(spaceId, marker, targetHash) {
|
|
2000
|
+
const fromHash = marker?.storageHash ?? "<empty>";
|
|
2001
|
+
return {
|
|
2002
|
+
code: "MIGRATION_PATH_NOT_FOUND",
|
|
2003
|
+
summary: spaceId === "app" ? "Current contract has no planned migration path" : `Current contract has no planned migration path for contract space "${spaceId}"`,
|
|
2004
|
+
why: `No migration edge connects the current state "${fromHash}" to the target "${targetHash}" in contract space "${spaceId}". The on-disk migration graph does not join the two, and migrate replays existing edges — it never invents one.`,
|
|
2005
|
+
meta: {
|
|
2006
|
+
spaceId,
|
|
2007
|
+
fromHash,
|
|
2008
|
+
targetHash,
|
|
2009
|
+
kind: "pathUnreachable"
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Creates a programmatic control client for Prisma Next operations.
|
|
2015
|
+
*
|
|
2016
|
+
* The client accepts framework component descriptors at creation time,
|
|
2017
|
+
* manages driver lifecycle via connect()/close(), and exposes domain
|
|
2018
|
+
* operations that delegate to the existing family instance methods.
|
|
2019
|
+
*
|
|
2020
|
+
* @see {@link ControlClient} for the client interface
|
|
2021
|
+
* @see README.md "Programmatic Control API" section for usage examples
|
|
2022
|
+
*/
|
|
2023
|
+
function createControlClient(options) {
|
|
2024
|
+
return new ControlClientImpl(options);
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Implementation of ControlClient.
|
|
2028
|
+
* Manages initialization and connection state, delegates operations to family instance.
|
|
2029
|
+
*/
|
|
2030
|
+
var ControlClientImpl = class {
|
|
2031
|
+
options;
|
|
2032
|
+
stack = null;
|
|
2033
|
+
driver = null;
|
|
2034
|
+
familyInstance = null;
|
|
2035
|
+
frameworkComponents = null;
|
|
2036
|
+
initialized = false;
|
|
2037
|
+
defaultConnection;
|
|
2038
|
+
constructor(options) {
|
|
2039
|
+
this.options = options;
|
|
2040
|
+
this.defaultConnection = options.connection;
|
|
2041
|
+
}
|
|
2042
|
+
init() {
|
|
2043
|
+
if (this.initialized) return;
|
|
2044
|
+
this.stack = createControlStack({
|
|
2045
|
+
family: this.options.family,
|
|
2046
|
+
target: this.options.target,
|
|
2047
|
+
adapter: this.options.adapter,
|
|
2048
|
+
driver: this.options.driver,
|
|
2049
|
+
extensions: this.options.extensions
|
|
2050
|
+
});
|
|
2051
|
+
this.familyInstance = this.options.family.create(this.stack);
|
|
2052
|
+
const rawComponents = [
|
|
2053
|
+
this.options.target,
|
|
2054
|
+
this.options.adapter,
|
|
2055
|
+
...this.options.extensions ?? []
|
|
2056
|
+
];
|
|
2057
|
+
this.frameworkComponents = assertFrameworkComponentsCompatible(this.options.family.familyId, this.options.target.targetId, rawComponents);
|
|
2058
|
+
this.initialized = true;
|
|
2059
|
+
}
|
|
2060
|
+
async connect(connection) {
|
|
2061
|
+
this.init();
|
|
2062
|
+
if (this.driver) throw new CliStructuredError("DRIVER.ALREADY_CONNECTED", "Already connected. Call close() before reconnecting.");
|
|
2063
|
+
const resolvedConnection = connection ?? this.defaultConnection;
|
|
2064
|
+
if (resolvedConnection === void 0) throw new CliStructuredError("CONFIG.DB_CONNECTION_REQUIRED", "No connection provided. Pass a connection to connect() or provide a default connection when creating the client.");
|
|
2065
|
+
if (!this.stack?.driver) throw new CliStructuredError("CONFIG.DRIVER_REQUIRED", "Driver is not configured. Pass a driver descriptor when creating the control client to enable database operations.");
|
|
2066
|
+
this.driver = await this.stack.driver.create(resolvedConnection);
|
|
2067
|
+
}
|
|
2068
|
+
async close() {
|
|
2069
|
+
if (this.driver) {
|
|
2070
|
+
await this.driver.close();
|
|
2071
|
+
this.driver = null;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Construct the control adapter once for a migration operation and return
|
|
2076
|
+
* it, mirroring how the runtime plane builds the execution adapter once in
|
|
2077
|
+
* `createExecutionStack`. Only `dbInit` / `dbUpdate` need it (it lowers the
|
|
2078
|
+
* planner's DDL); read-only operations never build it. The descriptor is
|
|
2079
|
+
* optional on the stack — targets without migrations omit it.
|
|
2080
|
+
*/
|
|
2081
|
+
buildControlAdapter() {
|
|
2082
|
+
this.init();
|
|
2083
|
+
if (!this.stack?.adapter) throw new CliStructuredError("MIGRATION.TARGET_UNSUPPORTED", `Target "${this.options.target.targetId}" requires an adapter for migrations`);
|
|
2084
|
+
return this.stack.adapter.create(this.stack);
|
|
2085
|
+
}
|
|
2086
|
+
async ensureConnected() {
|
|
2087
|
+
this.init();
|
|
2088
|
+
if (!this.driver && this.defaultConnection !== void 0) await this.connect(this.defaultConnection);
|
|
2089
|
+
if (!this.driver || !this.familyInstance || !this.frameworkComponents) throw new CliStructuredError("DRIVER.NOT_CONNECTED", "Not connected. Call connect(connection) first.");
|
|
2090
|
+
return {
|
|
2091
|
+
driver: this.driver,
|
|
2092
|
+
familyInstance: this.familyInstance,
|
|
2093
|
+
frameworkComponents: this.frameworkComponents
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
async connectWithProgress(connection, action, onProgress) {
|
|
2097
|
+
if (connection === void 0) return;
|
|
2098
|
+
onProgress?.({
|
|
2099
|
+
action,
|
|
2100
|
+
kind: "spanStart",
|
|
2101
|
+
spanId: "connect",
|
|
2102
|
+
label: "Connecting to database..."
|
|
2103
|
+
});
|
|
2104
|
+
try {
|
|
2105
|
+
await this.connect(connection);
|
|
2106
|
+
onProgress?.({
|
|
2107
|
+
action,
|
|
2108
|
+
kind: "spanEnd",
|
|
2109
|
+
spanId: "connect",
|
|
2110
|
+
outcome: "ok"
|
|
2111
|
+
});
|
|
2112
|
+
} catch (error) {
|
|
2113
|
+
onProgress?.({
|
|
2114
|
+
action,
|
|
2115
|
+
kind: "spanEnd",
|
|
2116
|
+
spanId: "connect",
|
|
2117
|
+
outcome: "error"
|
|
2118
|
+
});
|
|
2119
|
+
throw error;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
async verify(options) {
|
|
2123
|
+
const { onProgress } = options;
|
|
2124
|
+
await this.connectWithProgress(options.connection, "verify", onProgress);
|
|
2125
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
2126
|
+
let contract;
|
|
2127
|
+
try {
|
|
2128
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
throw structuredError("CONTRACT.VALIDATION_FAILED", error instanceof Error ? error.message : String(error), { cause: error });
|
|
2131
|
+
}
|
|
2132
|
+
onProgress?.({
|
|
2133
|
+
action: "verify",
|
|
2134
|
+
kind: "spanStart",
|
|
2135
|
+
spanId: "verify",
|
|
2136
|
+
label: "Verifying database marker..."
|
|
2137
|
+
});
|
|
2138
|
+
try {
|
|
2139
|
+
const result = await familyInstance.verify({
|
|
2140
|
+
driver,
|
|
2141
|
+
contract,
|
|
2142
|
+
expectedTargetId: this.options.target.targetId,
|
|
2143
|
+
contractPath: ""
|
|
2144
|
+
});
|
|
2145
|
+
onProgress?.({
|
|
2146
|
+
action: "verify",
|
|
2147
|
+
kind: "spanEnd",
|
|
2148
|
+
spanId: "verify",
|
|
2149
|
+
outcome: result.ok ? "ok" : "error"
|
|
2150
|
+
});
|
|
2151
|
+
return result;
|
|
2152
|
+
} catch (error) {
|
|
2153
|
+
onProgress?.({
|
|
2154
|
+
action: "verify",
|
|
2155
|
+
kind: "spanEnd",
|
|
2156
|
+
spanId: "verify",
|
|
2157
|
+
outcome: "error"
|
|
2158
|
+
});
|
|
2159
|
+
throw error;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
async schemaVerify(options) {
|
|
2163
|
+
const { onProgress } = options;
|
|
2164
|
+
await this.connectWithProgress(options.connection, "schemaVerify", onProgress);
|
|
2165
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
2166
|
+
let contract;
|
|
2167
|
+
try {
|
|
2168
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
2169
|
+
} catch (error) {
|
|
2170
|
+
throw structuredError("CONTRACT.VALIDATION_FAILED", error instanceof Error ? error.message : String(error), { cause: error });
|
|
2171
|
+
}
|
|
2172
|
+
onProgress?.({
|
|
2173
|
+
action: "schemaVerify",
|
|
2174
|
+
kind: "spanStart",
|
|
2175
|
+
spanId: "schemaVerify",
|
|
2176
|
+
label: "Verifying database schema..."
|
|
2177
|
+
});
|
|
2178
|
+
try {
|
|
2179
|
+
const schema = await familyInstance.introspect({
|
|
2180
|
+
driver,
|
|
2181
|
+
contract
|
|
2182
|
+
});
|
|
2183
|
+
const result = familyInstance.verifySchema({
|
|
2184
|
+
contract,
|
|
2185
|
+
schema,
|
|
2186
|
+
strict: options.strict ?? false,
|
|
2187
|
+
frameworkComponents
|
|
2188
|
+
});
|
|
2189
|
+
onProgress?.({
|
|
2190
|
+
action: "schemaVerify",
|
|
2191
|
+
kind: "spanEnd",
|
|
2192
|
+
spanId: "schemaVerify",
|
|
2193
|
+
outcome: result.ok ? "ok" : "error"
|
|
2194
|
+
});
|
|
2195
|
+
return result;
|
|
2196
|
+
} catch (error) {
|
|
2197
|
+
onProgress?.({
|
|
2198
|
+
action: "schemaVerify",
|
|
2199
|
+
kind: "spanEnd",
|
|
2200
|
+
spanId: "schemaVerify",
|
|
2201
|
+
outcome: "error"
|
|
2202
|
+
});
|
|
2203
|
+
throw error;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
async sign(options) {
|
|
2207
|
+
const { onProgress } = options;
|
|
2208
|
+
await this.connectWithProgress(options.connection, "sign", onProgress);
|
|
2209
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
2210
|
+
let contract;
|
|
2211
|
+
try {
|
|
2212
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
throw structuredError("CONTRACT.VALIDATION_FAILED", error instanceof Error ? error.message : String(error), { cause: error });
|
|
2215
|
+
}
|
|
2216
|
+
onProgress?.({
|
|
2217
|
+
action: "sign",
|
|
2218
|
+
kind: "spanStart",
|
|
2219
|
+
spanId: "sign",
|
|
2220
|
+
label: "Signing database..."
|
|
2221
|
+
});
|
|
2222
|
+
try {
|
|
2223
|
+
const result = await familyInstance.sign({
|
|
2224
|
+
driver,
|
|
2225
|
+
contract,
|
|
2226
|
+
contractPath: options.contractPath ?? "",
|
|
2227
|
+
...ifDefined("configPath", options.configPath)
|
|
2228
|
+
});
|
|
2229
|
+
onProgress?.({
|
|
2230
|
+
action: "sign",
|
|
2231
|
+
kind: "spanEnd",
|
|
2232
|
+
spanId: "sign",
|
|
2233
|
+
outcome: "ok"
|
|
2234
|
+
});
|
|
2235
|
+
return result;
|
|
2236
|
+
} catch (error) {
|
|
2237
|
+
onProgress?.({
|
|
2238
|
+
action: "sign",
|
|
2239
|
+
kind: "spanEnd",
|
|
2240
|
+
spanId: "sign",
|
|
2241
|
+
outcome: "error"
|
|
2242
|
+
});
|
|
2243
|
+
throw error;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
async dbInit(options) {
|
|
2247
|
+
const { onProgress } = options;
|
|
2248
|
+
await this.connectWithProgress(options.connection, "dbInit", onProgress);
|
|
2249
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
2250
|
+
if (!hasMigrations(this.options.target)) throw new CliStructuredError("MIGRATION.TARGET_UNSUPPORTED", `Target "${this.options.target.targetId}" does not support migrations`);
|
|
2251
|
+
const adapter = this.buildControlAdapter();
|
|
2252
|
+
let contract;
|
|
2253
|
+
try {
|
|
2254
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
2255
|
+
} catch (error) {
|
|
2256
|
+
throw structuredError("CONTRACT.VALIDATION_FAILED", error instanceof Error ? error.message : String(error), { cause: error });
|
|
2257
|
+
}
|
|
2258
|
+
return executeDbInit({
|
|
2259
|
+
driver,
|
|
2260
|
+
adapter,
|
|
2261
|
+
familyInstance,
|
|
2262
|
+
contract,
|
|
2263
|
+
mode: options.mode,
|
|
2264
|
+
migrations: this.options.target.migrations,
|
|
2265
|
+
frameworkComponents,
|
|
2266
|
+
migrationsDir: options.migrationsDir,
|
|
2267
|
+
targetId: this.options.target.targetId,
|
|
2268
|
+
extensions: this.options.extensions ?? [],
|
|
2269
|
+
...ifDefined("onProgress", onProgress)
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
async dbUpdate(options) {
|
|
2273
|
+
const { onProgress } = options;
|
|
2274
|
+
await this.connectWithProgress(options.connection, "dbUpdate", onProgress);
|
|
2275
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
2276
|
+
if (!hasMigrations(this.options.target)) throw new CliStructuredError("MIGRATION.TARGET_UNSUPPORTED", `Target "${this.options.target.targetId}" does not support migrations`);
|
|
2277
|
+
const adapter = this.buildControlAdapter();
|
|
2278
|
+
let contract;
|
|
2279
|
+
try {
|
|
2280
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
throw structuredError("CONTRACT.VALIDATION_FAILED", error instanceof Error ? error.message : String(error), { cause: error });
|
|
2283
|
+
}
|
|
2284
|
+
return executeDbUpdate({
|
|
2285
|
+
driver,
|
|
2286
|
+
adapter,
|
|
2287
|
+
familyInstance,
|
|
2288
|
+
contract,
|
|
2289
|
+
mode: options.mode,
|
|
2290
|
+
migrations: this.options.target.migrations,
|
|
2291
|
+
frameworkComponents,
|
|
2292
|
+
migrationsDir: options.migrationsDir,
|
|
2293
|
+
targetId: this.options.target.targetId,
|
|
2294
|
+
extensions: this.options.extensions ?? [],
|
|
2295
|
+
...ifDefined("acceptDataLoss", options.acceptDataLoss),
|
|
2296
|
+
...ifDefined("consent", options.consent),
|
|
2297
|
+
...ifDefined("onProgress", onProgress)
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
async dbVerify(options) {
|
|
2301
|
+
const { onProgress } = options;
|
|
2302
|
+
await this.connectWithProgress(options.connection, "dbVerify", onProgress);
|
|
2303
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
2304
|
+
return executeDbVerify({
|
|
2305
|
+
driver,
|
|
2306
|
+
familyInstance,
|
|
2307
|
+
contract: options.contract,
|
|
2308
|
+
migrationsDir: options.migrationsDir,
|
|
2309
|
+
targetId: this.options.target.targetId,
|
|
2310
|
+
extensions: this.options.extensions ?? [],
|
|
2311
|
+
frameworkComponents,
|
|
2312
|
+
mode: options.strict ? "strict" : "lenient",
|
|
2313
|
+
skipSchema: options.skipSchema,
|
|
2314
|
+
skipMarker: options.skipMarker,
|
|
2315
|
+
...ifDefined("onProgress", onProgress)
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
async readMarker() {
|
|
2319
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
2320
|
+
return familyInstance.readMarker({
|
|
2321
|
+
driver,
|
|
2322
|
+
space: APP_SPACE_ID
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
async readAllMarkers() {
|
|
2326
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
2327
|
+
return familyInstance.readAllMarkers({ driver });
|
|
2328
|
+
}
|
|
2329
|
+
/** Reads the per-migration journal; omit `space` to return every space. */
|
|
2330
|
+
async readLedger(space) {
|
|
2331
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
2332
|
+
return familyInstance.readLedger({
|
|
2333
|
+
driver,
|
|
2334
|
+
...ifDefined("space", space)
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
async migrate(options) {
|
|
2338
|
+
const { onProgress } = options;
|
|
2339
|
+
await this.connectWithProgress(options.connection, "migrate", onProgress);
|
|
2340
|
+
const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
|
|
2341
|
+
if (!hasMigrations(this.options.target)) throw new CliStructuredError("MIGRATION.TARGET_UNSUPPORTED", `Target "${this.options.target.targetId}" does not support migrations`);
|
|
2342
|
+
let contract;
|
|
2343
|
+
try {
|
|
2344
|
+
contract = familyInstance.deserializeContract(options.contract);
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
throw structuredError("CONTRACT.VALIDATION_FAILED", error instanceof Error ? error.message : String(error), { cause: error });
|
|
2347
|
+
}
|
|
2348
|
+
return executeMigrate({
|
|
2349
|
+
driver,
|
|
2350
|
+
familyInstance,
|
|
2351
|
+
contract,
|
|
2352
|
+
migrations: this.options.target.migrations,
|
|
2353
|
+
frameworkComponents,
|
|
2354
|
+
migrationsDir: options.migrationsDir,
|
|
2355
|
+
extensions: this.options.extensions ?? [],
|
|
2356
|
+
targetId: this.options.target.targetId,
|
|
2357
|
+
...ifDefined("refHash", options.refHash),
|
|
2358
|
+
...ifDefined("refInvariants", options.refInvariants),
|
|
2359
|
+
...ifDefined("refName", options.refName),
|
|
2360
|
+
...ifDefined("onProgress", onProgress)
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
async introspect(options) {
|
|
2364
|
+
const onProgress = options?.onProgress;
|
|
2365
|
+
await this.connectWithProgress(options?.connection, "introspect", onProgress);
|
|
2366
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
2367
|
+
options?.schema;
|
|
2368
|
+
onProgress?.({
|
|
2369
|
+
action: "introspect",
|
|
2370
|
+
kind: "spanStart",
|
|
2371
|
+
spanId: "introspect",
|
|
2372
|
+
label: "Introspecting database schema..."
|
|
2373
|
+
});
|
|
2374
|
+
try {
|
|
2375
|
+
const result = await familyInstance.introspect({ driver });
|
|
2376
|
+
onProgress?.({
|
|
2377
|
+
action: "introspect",
|
|
2378
|
+
kind: "spanEnd",
|
|
2379
|
+
spanId: "introspect",
|
|
2380
|
+
outcome: "ok"
|
|
2381
|
+
});
|
|
2382
|
+
return result;
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
onProgress?.({
|
|
2385
|
+
action: "introspect",
|
|
2386
|
+
kind: "spanEnd",
|
|
2387
|
+
spanId: "introspect",
|
|
2388
|
+
outcome: "error"
|
|
2389
|
+
});
|
|
2390
|
+
throw error;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
toSchemaView(schemaIR) {
|
|
2394
|
+
this.init();
|
|
2395
|
+
if (this.familyInstance && hasSchemaView(this.familyInstance)) return this.familyInstance.toSchemaView(schemaIR);
|
|
2396
|
+
}
|
|
2397
|
+
inferPslContract(schemaIR) {
|
|
2398
|
+
this.init();
|
|
2399
|
+
if (this.familyInstance && hasPslContractInfer(this.familyInstance)) return this.familyInstance.inferPslContract(schemaIR);
|
|
2400
|
+
}
|
|
2401
|
+
getPslBlockDescriptors() {
|
|
2402
|
+
this.init();
|
|
2403
|
+
return this.stack.authoringContributions.pslBlockDescriptors;
|
|
2404
|
+
}
|
|
2405
|
+
toOperationPreview(operations) {
|
|
2406
|
+
this.init();
|
|
2407
|
+
if (this.familyInstance && hasOperationPreview(this.familyInstance)) return this.familyInstance.toOperationPreview(operations);
|
|
2408
|
+
}
|
|
2409
|
+
async emit(options) {
|
|
2410
|
+
const { onProgress, contractConfig } = options;
|
|
2411
|
+
this.init();
|
|
2412
|
+
if (!this.familyInstance) throw new InternalError("Family instance was not initialized. This is a bug.");
|
|
2413
|
+
let contractRaw;
|
|
2414
|
+
onProgress?.({
|
|
2415
|
+
action: "emit",
|
|
2416
|
+
kind: "spanStart",
|
|
2417
|
+
spanId: "resolveSource",
|
|
2418
|
+
label: "Resolving contract source..."
|
|
2419
|
+
});
|
|
2420
|
+
try {
|
|
2421
|
+
const stack = this.stack;
|
|
2422
|
+
const sourceContext = {
|
|
2423
|
+
composedExtensions: stack.extensions.map((p) => p.id),
|
|
2424
|
+
composedExtensionContracts: stack.extensionContracts,
|
|
2425
|
+
authoringContributions: stack.authoringContributions,
|
|
2426
|
+
codecLookup: stack.codecLookup,
|
|
2427
|
+
controlMutationDefaults: stack.controlMutationDefaults,
|
|
2428
|
+
resolvedInputs: contractConfig.source.inputs ?? [],
|
|
2429
|
+
capabilities: stack.capabilities
|
|
2430
|
+
};
|
|
2431
|
+
const providerResult = await contractConfig.source.load(sourceContext);
|
|
2432
|
+
if (!providerResult.ok) {
|
|
2433
|
+
onProgress?.({
|
|
2434
|
+
action: "emit",
|
|
2435
|
+
kind: "spanEnd",
|
|
2436
|
+
spanId: "resolveSource",
|
|
2437
|
+
outcome: "error"
|
|
2438
|
+
});
|
|
2439
|
+
return notOk({
|
|
2440
|
+
code: "CONTRACT_SOURCE_INVALID",
|
|
2441
|
+
summary: providerResult.failure.summary,
|
|
2442
|
+
why: providerResult.failure.summary,
|
|
2443
|
+
meta: providerResult.failure.meta,
|
|
2444
|
+
diagnostics: providerResult.failure
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
contractRaw = providerResult.value;
|
|
2448
|
+
onProgress?.({
|
|
2449
|
+
action: "emit",
|
|
2450
|
+
kind: "spanEnd",
|
|
2451
|
+
spanId: "resolveSource",
|
|
2452
|
+
outcome: "ok"
|
|
2453
|
+
});
|
|
2454
|
+
} catch (error) {
|
|
2455
|
+
onProgress?.({
|
|
2456
|
+
action: "emit",
|
|
2457
|
+
kind: "spanEnd",
|
|
2458
|
+
spanId: "resolveSource",
|
|
2459
|
+
outcome: "error"
|
|
2460
|
+
});
|
|
2461
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2462
|
+
return notOk({
|
|
2463
|
+
code: "CONTRACT_SOURCE_INVALID",
|
|
2464
|
+
summary: "Failed to resolve contract source",
|
|
2465
|
+
why: message,
|
|
2466
|
+
diagnostics: {
|
|
2467
|
+
summary: "Contract source provider threw an exception",
|
|
2468
|
+
diagnostics: [{
|
|
2469
|
+
code: "PROVIDER_THROW",
|
|
2470
|
+
message
|
|
2471
|
+
}]
|
|
2472
|
+
},
|
|
2473
|
+
meta: void 0
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
onProgress?.({
|
|
2477
|
+
action: "emit",
|
|
2478
|
+
kind: "spanStart",
|
|
2479
|
+
spanId: "emit",
|
|
2480
|
+
label: "Emitting contract..."
|
|
2481
|
+
});
|
|
2482
|
+
try {
|
|
2483
|
+
const enrichedIR = enrichContract(contractRaw, this.frameworkComponents ?? []);
|
|
2484
|
+
const rawContractJson = this.options.target.contractSerializer.serializeContract(enrichedIR);
|
|
2485
|
+
let deserializedContract;
|
|
2486
|
+
try {
|
|
2487
|
+
deserializedContract = this.familyInstance.deserializeContract(rawContractJson);
|
|
2488
|
+
} catch (error) {
|
|
2489
|
+
onProgress?.({
|
|
2490
|
+
action: "emit",
|
|
2491
|
+
kind: "spanEnd",
|
|
2492
|
+
spanId: "emit",
|
|
2493
|
+
outcome: "error"
|
|
2494
|
+
});
|
|
2495
|
+
return notOk({
|
|
2496
|
+
code: "CONTRACT_VALIDATION_FAILED",
|
|
2497
|
+
summary: "Contract validation failed",
|
|
2498
|
+
why: error instanceof Error ? error.message : String(error),
|
|
2499
|
+
meta: void 0
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
const result = await emit(deserializedContract, this.stack, this.options.family.emission, { serializeContract: (contract) => this.options.target.contractSerializer.serializeContract(contract) });
|
|
2503
|
+
onProgress?.({
|
|
2504
|
+
action: "emit",
|
|
2505
|
+
kind: "spanEnd",
|
|
2506
|
+
spanId: "emit",
|
|
2507
|
+
outcome: "ok"
|
|
2508
|
+
});
|
|
2509
|
+
return ok({
|
|
2510
|
+
storageHash: result.storageHash,
|
|
2511
|
+
...ifDefined("executionHash", result.executionHash),
|
|
2512
|
+
profileHash: result.profileHash,
|
|
2513
|
+
contractJson: result.contractJson,
|
|
2514
|
+
contractDts: result.contractDts
|
|
2515
|
+
});
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
onProgress?.({
|
|
2518
|
+
action: "emit",
|
|
2519
|
+
kind: "spanEnd",
|
|
2520
|
+
spanId: "emit",
|
|
2521
|
+
outcome: "error"
|
|
2522
|
+
});
|
|
2523
|
+
return notOk({
|
|
2524
|
+
code: "EMIT_FAILED",
|
|
2525
|
+
summary: "Failed to emit contract",
|
|
2526
|
+
why: error instanceof Error ? error.message : String(error),
|
|
2527
|
+
meta: void 0
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
/**
|
|
2533
|
+
* Per-output FIFO queue for `executeContractEmit`.
|
|
2534
|
+
*
|
|
2535
|
+
* Ensures that at most one emit (load → resolve source → emit bytes → publish)
|
|
2536
|
+
* runs per output JSON path at a time. Concurrent calls for the same path
|
|
2537
|
+
* line up behind the in-flight one and run in submission order; the user-visible
|
|
2538
|
+
* outcome is "last submission wins on disk" without any supersession bookkeeping.
|
|
2539
|
+
*
|
|
2540
|
+
* Long-lived hosts (Vite dev server, watch CLIs) must call `disposeEmitQueue`
|
|
2541
|
+
* when they stop publishing to a path, otherwise the module-global `Map`
|
|
2542
|
+
* accumulates one entry per unique output path for the lifetime of the process.
|
|
2543
|
+
*/
|
|
2544
|
+
const emitQueues = /* @__PURE__ */ new Map();
|
|
2545
|
+
function queueEmitByOutput(outputJsonPath, action) {
|
|
2546
|
+
const next = (emitQueues.get(outputJsonPath) ?? Promise.resolve()).then(action, action);
|
|
2547
|
+
emitQueues.set(outputJsonPath, next);
|
|
2548
|
+
return next;
|
|
2549
|
+
}
|
|
2550
|
+
function disposeEmitQueue(outputJsonPath) {
|
|
2551
|
+
emitQueues.delete(outputJsonPath);
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* The import root of the project being emitted for, read from its own
|
|
2555
|
+
* manifest.
|
|
2556
|
+
*
|
|
2557
|
+
* Generated files are kept by the application, so every package name in them
|
|
2558
|
+
* has to be one the application can resolve — which means one of its direct
|
|
2559
|
+
* dependencies (ADR 242). The application already states those in
|
|
2560
|
+
* `package.json`, so emission reads them instead of taking a mode from
|
|
2561
|
+
* configuration that could disagree with what is installed.
|
|
2562
|
+
*/
|
|
2563
|
+
const DEPENDENCY_FIELDS = ["dependencies", "devDependencies"];
|
|
2564
|
+
function isRecord$2(value) {
|
|
2565
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2566
|
+
}
|
|
2567
|
+
/** The error codes that mean "no manifest here", as opposed to "cannot read it". */
|
|
2568
|
+
const MANIFEST_ABSENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
|
|
2569
|
+
function errorCode(error) {
|
|
2570
|
+
if (!isRecord$2(error)) return void 0;
|
|
2571
|
+
const code = error["code"];
|
|
2572
|
+
return typeof code === "string" ? code : void 0;
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* The nearest `package.json` at or above `from`, or `undefined` if there is
|
|
2576
|
+
* none. The nearest one wins: a package inside a workspace states its own
|
|
2577
|
+
* dependencies, and the workspace root's are somebody else's.
|
|
2578
|
+
*
|
|
2579
|
+
* @throws {CliStructuredError} when a manifest is there but cannot be used —
|
|
2580
|
+
* unreadable, not valid JSON, or not a JSON object. Only "there is no manifest
|
|
2581
|
+
* at this level" continues the walk up; treating a permissions failure that
|
|
2582
|
+
* way would silently emit against the wrong project's dependencies.
|
|
2583
|
+
*/
|
|
2584
|
+
function nearestManifest(from) {
|
|
2585
|
+
let dir = from;
|
|
2586
|
+
while (true) {
|
|
2587
|
+
const path = join(dir, "package.json");
|
|
2588
|
+
let raw;
|
|
2589
|
+
try {
|
|
2590
|
+
raw = readFileSync(path, "utf8");
|
|
2591
|
+
} catch (cause) {
|
|
2592
|
+
if (!MANIFEST_ABSENT_CODES.has(errorCode(cause) ?? "")) throw errorRuntime("CLI.PROJECT_MANIFEST_UNREADABLE", `Failed to read ${path}`, {
|
|
2593
|
+
why: `\`${path}\` exists but could not be read: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
2594
|
+
fix: `Make \`${path}\` readable, then re-run the command. Emission reads it to decide which package names generated files should import.`,
|
|
2595
|
+
meta: { path },
|
|
2596
|
+
cause
|
|
2597
|
+
});
|
|
2598
|
+
const parent = dirname(dir);
|
|
2599
|
+
if (parent === dir) return void 0;
|
|
2600
|
+
dir = parent;
|
|
2601
|
+
continue;
|
|
2602
|
+
}
|
|
2603
|
+
let parsed;
|
|
2604
|
+
try {
|
|
2605
|
+
parsed = JSON.parse(raw);
|
|
2606
|
+
} catch (cause) {
|
|
2607
|
+
throw errorRuntime("CLI.PROJECT_MANIFEST_INVALID", `Failed to parse ${path}`, {
|
|
2608
|
+
why: `\`${path}\` is not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
2609
|
+
fix: `Fix the JSON syntax in \`${path}\` (a missing comma or unbalanced brace is the most common cause), then re-run the command. Emission reads it to decide which package names generated files should import.`,
|
|
2610
|
+
meta: { path },
|
|
2611
|
+
cause
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
if (!isRecord$2(parsed)) throw errorRuntime("CLI.PROJECT_MANIFEST_INVALID", `Failed to read ${path}`, {
|
|
2615
|
+
why: `\`${path}\` is valid JSON but not a JSON object, so it states no dependencies.`,
|
|
2616
|
+
fix: `Make \`${path}\` a JSON object with the usual manifest fields, then re-run the command. Emission reads it to decide which package names generated files should import.`,
|
|
2617
|
+
meta: { path }
|
|
2618
|
+
});
|
|
2619
|
+
return parsed;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
function declaredDependencies(manifest) {
|
|
2623
|
+
return DEPENDENCY_FIELDS.flatMap((field) => {
|
|
2624
|
+
const value = manifest[field];
|
|
2625
|
+
return isRecord$2(value) ? Object.keys(value) : [];
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* The import root for the project that owns `configPath`, or for the working
|
|
2630
|
+
* directory when the config was discovered rather than named.
|
|
2631
|
+
*
|
|
2632
|
+
* A project with no manifest, or one that names no published package, is on
|
|
2633
|
+
* the internal root — which emits every specifier exactly as authored, so a
|
|
2634
|
+
* project that has not moved to published names is unaffected.
|
|
2635
|
+
*/
|
|
2636
|
+
function projectImportRoot(configPath) {
|
|
2637
|
+
const manifest = nearestManifest(configPath === void 0 ? resolve(".") : dirname(resolve(configPath)));
|
|
2638
|
+
if (manifest === void 0) return internalImportRoot;
|
|
2639
|
+
return importRootForDependencies(declaredDependencies(manifest));
|
|
2640
|
+
}
|
|
2641
|
+
/** The specifier resolver emission should use for the project owning `configPath`. */
|
|
2642
|
+
function createProjectSpecifierResolver(configPath) {
|
|
2643
|
+
return createImportSpecifierResolver(projectImportRoot(configPath));
|
|
2644
|
+
}
|
|
2645
|
+
function isRecord$1(value) {
|
|
2646
|
+
return typeof value === "object" && value !== null;
|
|
2647
|
+
}
|
|
2648
|
+
function createTempArtifactPath(path, publicationToken, phase) {
|
|
2649
|
+
return join(dirname(path), `.${basename(path)}.${process.pid}.${publicationToken}.${phase}.tmp`);
|
|
2650
|
+
}
|
|
2651
|
+
async function readExistingArtifact(path) {
|
|
2652
|
+
try {
|
|
2653
|
+
return { content: await readFile(path, "utf-8") };
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
if (isRecord$1(error) && error["code"] === "ENOENT") return "remove";
|
|
2656
|
+
throw error;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
async function restoreArtifact(path, previous, publicationToken) {
|
|
2660
|
+
if (previous === "remove") {
|
|
2661
|
+
await rm(path, { force: true });
|
|
2662
|
+
return;
|
|
2663
|
+
}
|
|
2664
|
+
const restorePath = createTempArtifactPath(path, publicationToken, "rollback");
|
|
2665
|
+
await writeFile(restorePath, previous.content, "utf-8");
|
|
2666
|
+
try {
|
|
2667
|
+
await rename(restorePath, path);
|
|
2668
|
+
} finally {
|
|
2669
|
+
await rm(restorePath, { force: true });
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
function withRollbackFailureCause(error, rollbackFailures) {
|
|
2673
|
+
const rollbackCause = new AggregateError(rollbackFailures, "Failed to restore published artifacts");
|
|
2674
|
+
if (error instanceof Error) {
|
|
2675
|
+
Object.defineProperty(error, "cause", {
|
|
2676
|
+
value: rollbackCause,
|
|
2677
|
+
configurable: true,
|
|
2678
|
+
writable: true
|
|
2679
|
+
});
|
|
2680
|
+
return error;
|
|
2681
|
+
}
|
|
2682
|
+
return new Error(String(error), { cause: rollbackCause });
|
|
2683
|
+
}
|
|
2684
|
+
async function publishPairWithRollback(entries, publicationToken) {
|
|
2685
|
+
const replaced = [];
|
|
2686
|
+
try {
|
|
2687
|
+
for (const entry of entries) {
|
|
2688
|
+
await rename(entry.tempPath, entry.outputPath);
|
|
2689
|
+
replaced.push(entry);
|
|
2690
|
+
}
|
|
2691
|
+
} catch (error) {
|
|
2692
|
+
const rollbackFailures = (await Promise.allSettled(replaced.map((entry) => restoreArtifact(entry.outputPath, entry.previous, publicationToken)))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
2693
|
+
if (rollbackFailures.length > 0) throw withRollbackFailureCause(error, rollbackFailures);
|
|
2694
|
+
throw error;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
async function publishContractArtifactPair({ outputJsonPath, outputDtsPath, contractJson, contractDts, publicationToken, beforePublish }) {
|
|
2698
|
+
const tempJsonPath = createTempArtifactPath(outputJsonPath, publicationToken, "next");
|
|
2699
|
+
const tempDtsPath = createTempArtifactPath(outputDtsPath, publicationToken, "next");
|
|
2700
|
+
try {
|
|
2701
|
+
await writeFile(tempJsonPath, contractJson, "utf-8");
|
|
2702
|
+
await writeFile(tempDtsPath, contractDts, "utf-8");
|
|
2703
|
+
if (await beforePublish?.() === false) return false;
|
|
2704
|
+
const previousJson = await readExistingArtifact(outputJsonPath);
|
|
2705
|
+
await publishPairWithRollback([{
|
|
2706
|
+
tempPath: tempDtsPath,
|
|
2707
|
+
outputPath: outputDtsPath,
|
|
2708
|
+
previous: await readExistingArtifact(outputDtsPath)
|
|
2709
|
+
}, {
|
|
2710
|
+
tempPath: tempJsonPath,
|
|
2711
|
+
outputPath: outputJsonPath,
|
|
2712
|
+
previous: previousJson
|
|
2713
|
+
}], publicationToken);
|
|
2714
|
+
return true;
|
|
2715
|
+
} finally {
|
|
2716
|
+
await Promise.allSettled([rm(tempJsonPath, { force: true }), rm(tempDtsPath, { force: true })]);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
const IMPORT_PATTERN = /import\s+type\s+.*?\s+from\s+['"](@[^/]+\/[^/'"]+)/g;
|
|
2720
|
+
function extractPackageSpecifiers(dtsContent) {
|
|
2721
|
+
const packages = /* @__PURE__ */ new Set();
|
|
2722
|
+
for (const match of dtsContent.matchAll(IMPORT_PATTERN)) {
|
|
2723
|
+
const pkg = match[1];
|
|
2724
|
+
if (pkg) packages.add(pkg);
|
|
2725
|
+
}
|
|
2726
|
+
return [...packages];
|
|
2727
|
+
}
|
|
2728
|
+
function validateContractDeps(dtsContent, projectRoot) {
|
|
2729
|
+
const packages = extractPackageSpecifiers(dtsContent);
|
|
2730
|
+
const resolve = createRequire(`${projectRoot}/package.json`);
|
|
2731
|
+
const missing = [];
|
|
2732
|
+
for (const pkg of packages) try {
|
|
2733
|
+
resolve.resolve(`${pkg}/package.json`);
|
|
2734
|
+
} catch {
|
|
2735
|
+
missing.push(pkg);
|
|
2736
|
+
}
|
|
2737
|
+
if (missing.length === 0) return { missing };
|
|
2738
|
+
return {
|
|
2739
|
+
missing,
|
|
2740
|
+
warning: [
|
|
2741
|
+
"contract.d.ts imports types from packages that are not installed:",
|
|
2742
|
+
missing.map((p) => ` - ${p}`).join("\n"),
|
|
2743
|
+
"",
|
|
2744
|
+
"Install them with your package manager:",
|
|
2745
|
+
...missing.map((p) => ` ${p}`)
|
|
2746
|
+
].join("\n")
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
const EMIT_ACTION = "emit";
|
|
2750
|
+
const defaultContractEmitDependencies = { emit };
|
|
2751
|
+
function isRecord(value) {
|
|
2752
|
+
return typeof value === "object" && value !== null;
|
|
2753
|
+
}
|
|
2754
|
+
function startSpan(onProgress, spanId, label) {
|
|
2755
|
+
onProgress?.({
|
|
2756
|
+
action: EMIT_ACTION,
|
|
2757
|
+
kind: "spanStart",
|
|
2758
|
+
spanId,
|
|
2759
|
+
label
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
function endSpan(onProgress, spanId, outcome) {
|
|
2763
|
+
onProgress?.({
|
|
2764
|
+
action: EMIT_ACTION,
|
|
2765
|
+
kind: "spanEnd",
|
|
2766
|
+
spanId,
|
|
2767
|
+
outcome
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
function failedToResolveContractSource(why, fix, meta, cause) {
|
|
2771
|
+
return errorRuntime("CONTRACT.SOURCE_LOAD_FAILED", "Failed to resolve contract source", {
|
|
2772
|
+
why,
|
|
2773
|
+
fix,
|
|
2774
|
+
...ifDefined("meta", meta),
|
|
2775
|
+
...ifDefined("cause", cause)
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
function diagnosticLocationSuffix(diagnostic) {
|
|
2779
|
+
const sourceId = typeof diagnostic["sourceId"] === "string" ? diagnostic["sourceId"] : void 0;
|
|
2780
|
+
const span = isRecord(diagnostic["span"]) ? diagnostic["span"] : void 0;
|
|
2781
|
+
const start = span && isRecord(span["start"]) ? span["start"] : void 0;
|
|
2782
|
+
const line = start && typeof start["line"] === "number" ? start["line"] : void 0;
|
|
2783
|
+
const column = start && typeof start["column"] === "number" ? start["column"] : void 0;
|
|
2784
|
+
if (sourceId && line !== void 0 && column !== void 0) return ` (${sourceId}:${line}:${column})`;
|
|
2785
|
+
if (sourceId) return ` (${sourceId})`;
|
|
2786
|
+
return "";
|
|
2787
|
+
}
|
|
2788
|
+
function mapDiagnosticsToIssues(diagnostics) {
|
|
2789
|
+
const issues = [];
|
|
2790
|
+
for (const raw of diagnostics) {
|
|
2791
|
+
if (!isRecord(raw)) continue;
|
|
2792
|
+
const code = typeof raw["code"] === "string" ? raw["code"] : "diagnostic";
|
|
2793
|
+
const message = typeof raw["message"] === "string" ? raw["message"] : "";
|
|
2794
|
+
issues.push({
|
|
2795
|
+
kind: code,
|
|
2796
|
+
message: `${message}${diagnosticLocationSuffix(raw)}`
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
return issues;
|
|
2800
|
+
}
|
|
2801
|
+
function validateProviderResult(providerResult) {
|
|
2802
|
+
if (!isRecord(providerResult) || typeof providerResult["ok"] !== "boolean") return {
|
|
2803
|
+
ok: false,
|
|
2804
|
+
error: failedToResolveContractSource("Contract source provider returned malformed result shape.", "Ensure contract.source.load resolves to ok(Contract) or notOk({ summary, diagnostics }).")
|
|
2805
|
+
};
|
|
2806
|
+
if (providerResult["ok"]) {
|
|
2807
|
+
if (!("value" in providerResult)) return {
|
|
2808
|
+
ok: false,
|
|
2809
|
+
error: failedToResolveContractSource("Contract source provider returned malformed success result: missing value.", "Ensure contract.source.load success payload is ok(Contract).")
|
|
2810
|
+
};
|
|
2811
|
+
return {
|
|
2812
|
+
ok: true,
|
|
2813
|
+
value: providerResult["value"]
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
const failure = providerResult["failure"];
|
|
2817
|
+
if (!isRecord(failure) || typeof failure["summary"] !== "string" || !Array.isArray(failure["diagnostics"])) return {
|
|
2818
|
+
ok: false,
|
|
2819
|
+
error: failedToResolveContractSource("Contract source provider returned malformed failure result: expected summary and diagnostics.", "Ensure contract.source.load failure payload is notOk({ summary, diagnostics, meta? }).")
|
|
2820
|
+
};
|
|
2821
|
+
return {
|
|
2822
|
+
ok: false,
|
|
2823
|
+
error: failedToResolveContractSource(String(failure["summary"]), "Fix contract source diagnostics and return ok(Contract).", {
|
|
2824
|
+
diagnostics: failure["diagnostics"],
|
|
2825
|
+
issues: mapDiagnosticsToIssues(failure["diagnostics"]),
|
|
2826
|
+
...ifDefined("providerMeta", failure["meta"])
|
|
2827
|
+
})
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Canonical contract emit operation.
|
|
2832
|
+
*
|
|
2833
|
+
* This is the SINGLE publication path used by both the CLI command
|
|
2834
|
+
* (`prisma-next contract emit`) and the Vite plugin
|
|
2835
|
+
* (`@internal/vite-plugin-contract-emit`). New callers must go through this
|
|
2836
|
+
* function rather than re-implementing load → emit → publish.
|
|
2837
|
+
*
|
|
2838
|
+
* The whole flow (load config → resolve source → emit bytes → atomic publish)
|
|
2839
|
+
* is serialized per output JSON path via `queueEmitByOutput`. Concurrent calls
|
|
2840
|
+
* for the same output line up FIFO; the user-visible outcome is "last
|
|
2841
|
+
* submission wins on disk" without any supersession bookkeeping. Within a
|
|
2842
|
+
* single emit, `publishContractArtifactPair` stages temp files, renames
|
|
2843
|
+
* `contract.d.ts` before `contract.json`, and attempts to restore the previous
|
|
2844
|
+
* pair if either rename fails — so type-only consumers never observe a
|
|
2845
|
+
* mismatched pair.
|
|
2846
|
+
*
|
|
2847
|
+
* @throws {CliStructuredError} on config/source/validation problems
|
|
2848
|
+
* @throws {DOMException} `AbortError` if cancelled via `signal`
|
|
2849
|
+
*/
|
|
2850
|
+
async function executeContractEmit(options, dependencies = defaultContractEmitDependencies) {
|
|
2851
|
+
const { config, configPath, outputPath, signal = new AbortController().signal, onProgress } = options;
|
|
2852
|
+
const unlessAborted = abortable(signal);
|
|
2853
|
+
if (!config.contract) throw errorContractConfigMissing({ why: "Config.contract is required for emit. Define it in your config: contract: { source: ..., output: ... }" });
|
|
2854
|
+
const contractConfig = config.contract;
|
|
2855
|
+
const effectiveOutput = outputPath !== void 0 ? join(outputPath, "contract.json") : contractConfig.output;
|
|
2856
|
+
if (!effectiveOutput) throw errorContractConfigMissing({ why: "Contract config must have output path. This should not happen if defineConfig() was used." });
|
|
2857
|
+
if (typeof contractConfig.source?.load !== "function") throw errorContractConfigMissing({ why: "Contract config must include a valid source provider object" });
|
|
2858
|
+
let outputPaths;
|
|
2859
|
+
try {
|
|
2860
|
+
outputPaths = getEmittedArtifactPaths(effectiveOutput);
|
|
2861
|
+
} catch (error) {
|
|
2862
|
+
throw errorContractConfigMissing({ why: error instanceof Error ? error.message : String(error) });
|
|
2863
|
+
}
|
|
2864
|
+
const { jsonPath: outputJsonPath, dtsPath: outputDtsPath } = outputPaths;
|
|
2865
|
+
return queueEmitByOutput(outputJsonPath, async () => {
|
|
2866
|
+
const stack = createControlStack(config);
|
|
2867
|
+
const sourceContext = {
|
|
2868
|
+
composedExtensions: stack.extensions.map((p) => p.id),
|
|
2869
|
+
composedExtensionContracts: stack.extensionContracts,
|
|
2870
|
+
authoringContributions: stack.authoringContributions,
|
|
2871
|
+
codecLookup: stack.codecLookup,
|
|
2872
|
+
controlMutationDefaults: stack.controlMutationDefaults,
|
|
2873
|
+
resolvedInputs: contractConfig.source.inputs ?? [],
|
|
2874
|
+
capabilities: stack.capabilities
|
|
2875
|
+
};
|
|
2876
|
+
startSpan(onProgress, "resolveSource", "Resolving contract source...");
|
|
2877
|
+
let providerResult;
|
|
2878
|
+
try {
|
|
2879
|
+
providerResult = await unlessAborted(contractConfig.source.load(sourceContext));
|
|
2880
|
+
} catch (error) {
|
|
2881
|
+
endSpan(onProgress, "resolveSource", "error");
|
|
2882
|
+
if (signal.aborted || isRecord(error) && error["name"] === "AbortError") throw error;
|
|
2883
|
+
throw failedToResolveContractSource(error instanceof Error ? error.message : String(error), "Ensure contract.source.load resolves to ok(Contract) or returns structured diagnostics.", void 0, error);
|
|
2884
|
+
}
|
|
2885
|
+
const validatedContract = validateProviderResult(providerResult);
|
|
2886
|
+
if (!validatedContract.ok) {
|
|
2887
|
+
endSpan(onProgress, "resolveSource", "error");
|
|
2888
|
+
throw validatedContract.error;
|
|
2889
|
+
}
|
|
2890
|
+
endSpan(onProgress, "resolveSource", "ok");
|
|
2891
|
+
startSpan(onProgress, "emit", "Emitting contract...");
|
|
2892
|
+
let emitResult;
|
|
2893
|
+
try {
|
|
2894
|
+
const familyInstance = config.family.create(stack);
|
|
2895
|
+
const rawComponents = [
|
|
2896
|
+
config.target,
|
|
2897
|
+
config.adapter,
|
|
2898
|
+
...config.extensions ?? []
|
|
2899
|
+
];
|
|
2900
|
+
const frameworkComponents = assertFrameworkComponentsCompatible(config.family.familyId, config.target.targetId, rawComponents);
|
|
2901
|
+
const enrichedIR = enrichContract(validatedContract.value, frameworkComponents);
|
|
2902
|
+
const rawContractJson = config.target.contractSerializer.serializeContract(enrichedIR);
|
|
2903
|
+
const deserializedContract = familyInstance.deserializeContract(rawContractJson);
|
|
2904
|
+
const { contractSerializer } = config.target;
|
|
2905
|
+
const serializeContract = (c) => contractSerializer.serializeContract(c);
|
|
2906
|
+
emitResult = await unlessAborted(dependencies.emit(deserializedContract, stack, config.family.emission, {
|
|
2907
|
+
outputJsonPath,
|
|
2908
|
+
serializeContract,
|
|
2909
|
+
resolveImportSpecifier: createProjectSpecifierResolver(configPath ?? outputJsonPath),
|
|
2910
|
+
...ifDefined("shouldPreserveEmpty", contractSerializer.shouldPreserveEmpty),
|
|
2911
|
+
...ifDefined("sortStorage", contractSerializer.sortStorage)
|
|
2912
|
+
}));
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
endSpan(onProgress, "emit", "error");
|
|
2915
|
+
throw error;
|
|
2916
|
+
}
|
|
2917
|
+
endSpan(onProgress, "emit", "ok");
|
|
2918
|
+
await unlessAborted(mkdir(dirname(outputJsonPath), { recursive: true }));
|
|
2919
|
+
await publishContractArtifactPair({
|
|
2920
|
+
outputJsonPath,
|
|
2921
|
+
outputDtsPath,
|
|
2922
|
+
contractJson: emitResult.contractJson,
|
|
2923
|
+
contractDts: emitResult.contractDts,
|
|
2924
|
+
publicationToken: String(process.hrtime.bigint())
|
|
2925
|
+
});
|
|
2926
|
+
const validationWarning = validateContractDeps(emitResult.contractDts, dirname(outputDtsPath)).warning;
|
|
2927
|
+
return {
|
|
2928
|
+
storageHash: emitResult.storageHash,
|
|
2929
|
+
...ifDefined("executionHash", emitResult.executionHash),
|
|
2930
|
+
profileHash: emitResult.profileHash,
|
|
2931
|
+
files: {
|
|
2932
|
+
json: outputJsonPath,
|
|
2933
|
+
dts: outputDtsPath
|
|
2934
|
+
},
|
|
2935
|
+
...ifDefined("validationWarning", validationWarning)
|
|
2936
|
+
};
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
function computeRefAdvancementName(options) {
|
|
2940
|
+
if (options.advanceRef !== void 0) return options.advanceRef;
|
|
2941
|
+
if (options.db === void 0) return "db";
|
|
2942
|
+
return null;
|
|
2943
|
+
}
|
|
2944
|
+
async function readContractIR(contractJson, contractJsonPath) {
|
|
2945
|
+
return {
|
|
2946
|
+
contract: contractJson,
|
|
2947
|
+
contractDts: await readFile(contractJsonPath.replace(/\.json$/i, ".d.ts"), "utf-8")
|
|
2948
|
+
};
|
|
2949
|
+
}
|
|
2950
|
+
async function executeRefAdvancement(refsDir, migrationsDir, name, hash, contractIR) {
|
|
2951
|
+
if (!validateRefName(name)) throw errorInvalidRefName(name);
|
|
2952
|
+
await writeContractSnapshot(migrationsDir, hash, {
|
|
2953
|
+
contractJson: contractIR.contract,
|
|
2954
|
+
contractDts: contractIR.contractDts
|
|
2955
|
+
});
|
|
2956
|
+
await writeRef(refsDir, name, {
|
|
2957
|
+
hash,
|
|
2958
|
+
invariants: []
|
|
2959
|
+
});
|
|
2960
|
+
return {
|
|
2961
|
+
name,
|
|
2962
|
+
hash
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
async function buildRefAdvancementFields(options) {
|
|
2966
|
+
const name = computeRefAdvancementName({
|
|
2967
|
+
...ifDefined("advanceRef", options.advanceRef),
|
|
2968
|
+
...ifDefined("db", options.db)
|
|
2969
|
+
});
|
|
2970
|
+
if (name === null) return {
|
|
2971
|
+
advancedRef: null,
|
|
2972
|
+
plannedAdvanceRef: null
|
|
2973
|
+
};
|
|
2974
|
+
if (options.mode === "plan") return {
|
|
2975
|
+
advancedRef: null,
|
|
2976
|
+
plannedAdvanceRef: {
|
|
2977
|
+
name,
|
|
2978
|
+
hash: options.hash
|
|
2979
|
+
}
|
|
2980
|
+
};
|
|
2981
|
+
return {
|
|
2982
|
+
advancedRef: await executeRefAdvancement(options.refsDir, options.migrationsDir, name, options.hash, options.contractIR),
|
|
2983
|
+
plannedAdvanceRef: null
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Full ref-advancement phase for db init/update: ok({advancedRef:null, plannedAdvanceRef:null})
|
|
2988
|
+
* when computeRefAdvancementName is null; else readContractIR + buildRefAdvancementFields with
|
|
2989
|
+
* MigrationToolsError mapped, other errors rethrown.
|
|
2990
|
+
*/
|
|
2991
|
+
async function resolveRefAdvancementFields(options) {
|
|
2992
|
+
if (computeRefAdvancementName({
|
|
2993
|
+
...ifDefined("advanceRef", options.advanceRef),
|
|
2994
|
+
...ifDefined("db", options.db)
|
|
2995
|
+
}) === null) return ok({
|
|
2996
|
+
advancedRef: null,
|
|
2997
|
+
plannedAdvanceRef: null
|
|
2998
|
+
});
|
|
2999
|
+
try {
|
|
3000
|
+
const contractIR = await readContractIR(options.contractJson, options.contractJsonPath);
|
|
3001
|
+
return ok(await buildRefAdvancementFields({
|
|
3002
|
+
...ifDefined("advanceRef", options.advanceRef),
|
|
3003
|
+
...ifDefined("db", options.db),
|
|
3004
|
+
refsDir: options.refsDir,
|
|
3005
|
+
migrationsDir: options.migrationsDir,
|
|
3006
|
+
contractIR,
|
|
3007
|
+
mode: options.mode,
|
|
3008
|
+
hash: options.hash
|
|
3009
|
+
}));
|
|
3010
|
+
} catch (error) {
|
|
3011
|
+
if (MigrationToolsError.is(error)) return notOk(error);
|
|
3012
|
+
throw error;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
/** migrate's --advance-ref tail: executeRefAdvancement with MigrationToolsError passed through, others rethrown. */
|
|
3016
|
+
async function advanceRefSafely(args) {
|
|
3017
|
+
try {
|
|
3018
|
+
return ok(await executeRefAdvancement(args.refsDir, args.migrationsDir, args.name, args.hash, args.contractIR));
|
|
3019
|
+
} catch (error) {
|
|
3020
|
+
if (MigrationToolsError.is(error)) return notOk(error);
|
|
3021
|
+
throw error;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Resolves a user-supplied contract reference to an on-disk contract JSON (snapshot store or emitted contract), shared by db sign and db update --to.
|
|
3026
|
+
*/
|
|
3027
|
+
function isEnoent$1(error) {
|
|
3028
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
3029
|
+
}
|
|
3030
|
+
async function resolveContractRefToSnapshot(options) {
|
|
3031
|
+
try {
|
|
3032
|
+
const loaded = await buildReadAggregate(options.config, { migrationsDir: options.migrationsDir });
|
|
3033
|
+
if (!loaded.ok) return notOk(loaded.failure);
|
|
3034
|
+
const graph = loaded.value.aggregate.app.graph();
|
|
3035
|
+
const bundles = loaded.value.aggregate.app.packages;
|
|
3036
|
+
const refs = loaded.value.aggregate.app.refs;
|
|
3037
|
+
const refResult = parseContractRef(options.refInput, {
|
|
3038
|
+
graph,
|
|
3039
|
+
refs
|
|
3040
|
+
});
|
|
3041
|
+
if (!refResult.ok) return notOk(mapRefResolutionError(refResult.failure));
|
|
3042
|
+
const targetHash = refResult.value.hash;
|
|
3043
|
+
if (bundles.find((p) => p.metadata.to === targetHash)) return ok({
|
|
3044
|
+
hash: targetHash,
|
|
3045
|
+
contractJson: blindCast(await readContractSnapshotJson(options.migrationsDir, targetHash)),
|
|
3046
|
+
contractJsonPath: join(contractSnapshotDir(options.migrationsDir, targetHash), "contract.json"),
|
|
3047
|
+
source: "snapshot"
|
|
3048
|
+
});
|
|
3049
|
+
if (!options.fallbackToEmitted) return notOk(errorUnexpected(`No migration bundle found for ${options.missingBundleFlag} "${options.refInput}" (resolved hash: ${targetHash})`, {
|
|
3050
|
+
why: `The ref resolved successfully but no on-disk migration package has a destination (\`to\`) hash matching ${targetHash}.`,
|
|
3051
|
+
fix: "Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations."
|
|
3052
|
+
}));
|
|
3053
|
+
let defaultRaw;
|
|
3054
|
+
try {
|
|
3055
|
+
defaultRaw = await readFile(options.contractPathAbsolute, "utf-8");
|
|
3056
|
+
} catch (error) {
|
|
3057
|
+
if (isEnoent$1(error)) return notOk(errorFileNotFound(options.contractPathAbsolute, {
|
|
3058
|
+
why: `Contract file not found at ${options.contractPathAbsolute}`,
|
|
3059
|
+
fix: "Run `prisma-cli contract emit` first to generate the contract",
|
|
3060
|
+
cause: error
|
|
3061
|
+
}));
|
|
3062
|
+
return notOk(errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
3063
|
+
why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`,
|
|
3064
|
+
cause: error
|
|
3065
|
+
}));
|
|
3066
|
+
}
|
|
3067
|
+
let parsed;
|
|
3068
|
+
try {
|
|
3069
|
+
parsed = castAs(JSON.parse(defaultRaw));
|
|
3070
|
+
} catch (error) {
|
|
3071
|
+
return notOk(errorContractDeserializationFailed(options.contractPathAbsolute, error instanceof Error ? error.message : String(error), { cause: error }));
|
|
3072
|
+
}
|
|
3073
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return notOk(errorContractDeserializationFailed(options.contractPathAbsolute, `expected a JSON object, got ${parsed === null ? "null" : Array.isArray(parsed) ? "an array" : `a ${typeof parsed}`}`));
|
|
3074
|
+
const defaultContract = blindCast(parsed);
|
|
3075
|
+
const storage = defaultContract["storage"];
|
|
3076
|
+
if ((storage !== null && typeof storage === "object" ? blindCast(storage)["storageHash"] : void 0) === targetHash) return ok({
|
|
3077
|
+
hash: targetHash,
|
|
3078
|
+
contractJson: defaultContract,
|
|
3079
|
+
contractJsonPath: options.contractPathAbsolute,
|
|
3080
|
+
source: "emitted"
|
|
3081
|
+
});
|
|
3082
|
+
return notOk(errorRuntime("MIGRATION.SNAPSHOT_MISSING", `No contract file found for hash "${targetHash}"`, {
|
|
3083
|
+
why: `Resolved contract reference "${options.refInput}" to hash "${targetHash}" but no migration produces that hash and the emitted contract does not match.`,
|
|
3084
|
+
fix: "Ensure the target contract exists on disk — either as a migration endpoint or as the emitted contract.json."
|
|
3085
|
+
}));
|
|
3086
|
+
} catch (error) {
|
|
3087
|
+
if (MigrationToolsError.is(error)) return notOk(error);
|
|
3088
|
+
if (CliStructuredError$1.is(error)) return notOk(error);
|
|
3089
|
+
throw error;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
/**
|
|
3093
|
+
* Classifies errors caught around migration-tools calls so commands never import the MigrationToolsError class directly.
|
|
3094
|
+
*/
|
|
3095
|
+
/** CliStructuredError (including MigrationToolsError) → identity; anything else → null (caller rethrows/wraps). */
|
|
3096
|
+
function mapCaughtMigrationError(error) {
|
|
3097
|
+
if (CliStructuredError$1.is(error)) return error;
|
|
3098
|
+
return null;
|
|
3099
|
+
}
|
|
3100
|
+
function mapContractAtError(error, options) {
|
|
3101
|
+
if (MigrationToolsError.is(error)) switch (error.code) {
|
|
3102
|
+
case "MIGRATION.REF_NOT_RESOLVABLE": return notOk(errorSnapshotMissing(typeof error.meta?.["refName"] === "string" ? error.meta["refName"] : typeof error.meta?.["identifier"] === "string" ? error.meta["identifier"] : "unknown", { cause: error }));
|
|
3103
|
+
case "MIGRATION.CONTRACT_DESERIALIZATION_FAILED": {
|
|
3104
|
+
const filePath = typeof error.meta?.["filePath"] === "string" ? error.meta["filePath"] : "unknown";
|
|
3105
|
+
return notOk(errorContractValidationFailed(`Predecessor contract at ${filePath} failed to deserialize: ${typeof error.meta?.["message"] === "string" ? error.meta["message"] : error.message}`, {
|
|
3106
|
+
where: { path: filePath },
|
|
3107
|
+
cause: error
|
|
3108
|
+
}));
|
|
3109
|
+
}
|
|
3110
|
+
case "MIGRATION.INVALID_JSON": {
|
|
3111
|
+
const filePath = typeof error.meta?.["filePath"] === "string" ? error.meta["filePath"] : "unknown";
|
|
3112
|
+
const message = typeof error.meta?.["parseError"] === "string" ? error.meta["parseError"] : error.message;
|
|
3113
|
+
return notOk(errorContractValidationFailed((options?.artifactRole ?? "from") === "to" ? `Target contract at ${filePath} failed to deserialize: ${message}` : `Predecessor contract at ${filePath} failed to deserialize: ${message}`, {
|
|
3114
|
+
where: { path: filePath },
|
|
3115
|
+
cause: error
|
|
3116
|
+
}));
|
|
3117
|
+
}
|
|
3118
|
+
case "MIGRATION.BUNDLE_NOT_FOUND_FOR_GRAPH_NODE": return notOk(errorUnexpected(error.message, {
|
|
3119
|
+
why: error.why,
|
|
3120
|
+
fix: error.fix,
|
|
3121
|
+
cause: error
|
|
3122
|
+
}));
|
|
3123
|
+
case "MIGRATION.CONTRACT_SNAPSHOT_MISSING": {
|
|
3124
|
+
const expectedPath = typeof error.meta?.["expectedPath"] === "string" ? error.meta["expectedPath"] : "migrations/snapshots/";
|
|
3125
|
+
return notOk(errorFileNotFound(expectedPath, {
|
|
3126
|
+
why: (options?.artifactRole ?? "from") === "to" ? `Target migration is missing its contract snapshot at ${expectedPath}` : `Predecessor migration is missing its contract snapshot at ${expectedPath}`,
|
|
3127
|
+
fix: "Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.",
|
|
3128
|
+
cause: error
|
|
3129
|
+
}));
|
|
3130
|
+
}
|
|
3131
|
+
default: return notOk(error);
|
|
3132
|
+
}
|
|
3133
|
+
if (CliStructuredError$1.is(error)) return notOk(error);
|
|
3134
|
+
throw error;
|
|
3135
|
+
}
|
|
3136
|
+
/** True when the on-disk graph contains a path fromHash → toHash. */
|
|
3137
|
+
function hasMigrationPath(graph, fromHash, toHash) {
|
|
3138
|
+
return findPath(graph, fromHash, toHash) !== null;
|
|
3139
|
+
}
|
|
3140
|
+
/**
|
|
3141
|
+
* Refusal for a live marker hash that is not a node of the on-disk app graph.
|
|
3142
|
+
* Same errorMarkerMismatch envelope migrate raises today, or null when the marker is a node.
|
|
3143
|
+
*/
|
|
3144
|
+
function refuseMarkerOutsideGraph(args) {
|
|
3145
|
+
if (isGraphNode(args.markerHash, args.graph)) return null;
|
|
3146
|
+
return errorMarkerMismatch(args.markerHash, [...args.graph.nodes].sort(), findLatestMigration(args.graph)?.to ?? null);
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Invariant refusals shared by migrate and migration status: unknown ref invariants and unsatisfiable invariant paths.
|
|
3150
|
+
*/
|
|
3151
|
+
/** Refuses ref invariants neither declared on any graph edge nor present on the live marker. */
|
|
3152
|
+
function refuseUnknownInvariants(args) {
|
|
3153
|
+
const declared = collectDeclaredInvariants(args.graph);
|
|
3154
|
+
const known = new Set(declared);
|
|
3155
|
+
for (const id of args.markerInvariants) known.add(id);
|
|
3156
|
+
const unknown = args.refInvariants.filter((id) => !known.has(id));
|
|
3157
|
+
if (unknown.length === 0) return null;
|
|
3158
|
+
return errorUnknownInvariant({
|
|
3159
|
+
...ifDefined("refName", args.refName),
|
|
3160
|
+
unknown,
|
|
3161
|
+
declared: [...declared].sort()
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
/** Refuses a --to target whose missing invariants no path from originHash satisfies. */
|
|
3165
|
+
function refuseMissingInvariantPath(args) {
|
|
3166
|
+
const outcome = findPathWithDecision(args.graph, args.originHash, args.targetHash, {
|
|
3167
|
+
...ifDefined("refName", args.refName),
|
|
3168
|
+
required: new Set(args.missing)
|
|
3169
|
+
});
|
|
3170
|
+
if (outcome.kind !== "unsatisfiable") return null;
|
|
3171
|
+
return errorNoInvariantPath({
|
|
3172
|
+
...ifDefined("refName", args.refName),
|
|
3173
|
+
required: [...args.missing].sort(),
|
|
3174
|
+
missing: outcome.missing,
|
|
3175
|
+
structuralPath: outcome.structuralPath.map(toStructuralEdge)
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
/**
|
|
3179
|
+
* Computes the path through the SAME seam as `executeMigrate`:
|
|
3180
|
+
* - `readAllMarkers()` for the from-state (when no `--from` is given), preserving
|
|
3181
|
+
* the full marker including `invariants` (not just `storageHash`).
|
|
3182
|
+
* - `planSpacePath()` (shared with `executeMigrate`) for per-space path selection,
|
|
3183
|
+
* which feeds `resolveRecordedPath()` with the same target hash, target invariants,
|
|
3184
|
+
* and current marker as the real apply path uses.
|
|
3185
|
+
*
|
|
3186
|
+
* Returns BEFORE any write boundary (`runMigration` / marker / DDL). No
|
|
3187
|
+
* DB state is mutated.
|
|
3188
|
+
*/
|
|
3189
|
+
async function executeMigrateShowPlan(options) {
|
|
3190
|
+
const config = options.config;
|
|
3191
|
+
const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(options.configPath, config, options.cwd);
|
|
3192
|
+
const dbConnection = options.db ?? config.db?.connection;
|
|
3193
|
+
const hasDriver = !!config.driver;
|
|
3194
|
+
const hasExplicitFrom = options.from !== void 0;
|
|
3195
|
+
if (!hasExplicitFrom) {
|
|
3196
|
+
const missingDb = requireLiveDatabase({
|
|
3197
|
+
dbConnection,
|
|
3198
|
+
hasDriver,
|
|
3199
|
+
why: "migrate --show needs a database connection to read the live marker (or pass --from <contract> for an offline preview)",
|
|
3200
|
+
retryCommand: "prisma-cli migrate --show --from <contract>"
|
|
3201
|
+
});
|
|
3202
|
+
if (missingDb) return notOk(missingDb);
|
|
3203
|
+
}
|
|
3204
|
+
let allRefs = {};
|
|
3205
|
+
try {
|
|
3206
|
+
allRefs = await readRefs(refsDir);
|
|
3207
|
+
} catch (error) {
|
|
3208
|
+
if (MigrationToolsError.is(error)) return notOk(error);
|
|
3209
|
+
throw error;
|
|
3210
|
+
}
|
|
3211
|
+
const loaded = await buildReadAggregate(config, { migrationsDir });
|
|
3212
|
+
if (!loaded.ok) return notOk(loaded.failure);
|
|
3213
|
+
const { aggregate, contractHash } = loaded.value;
|
|
3214
|
+
const appGraph = aggregate.app.graph();
|
|
3215
|
+
let targetHash = contractHash;
|
|
3216
|
+
let refInvariants;
|
|
3217
|
+
if (options.to) {
|
|
3218
|
+
const toResult = parseContractRef(options.to, {
|
|
3219
|
+
graph: appGraph,
|
|
3220
|
+
refs: allRefs,
|
|
3221
|
+
contractHash
|
|
3222
|
+
});
|
|
3223
|
+
if (!toResult.ok) return notOk(mapRefResolutionError(toResult.failure));
|
|
3224
|
+
if (toResult.value.provenance.kind === "reserved-db") return notOk(errorDatabaseConnectionRequired({
|
|
3225
|
+
why: "@db is not valid as a --to target; it names the live database state, not a target contract.",
|
|
3226
|
+
commandName: "migrate --show"
|
|
3227
|
+
}));
|
|
3228
|
+
targetHash = toResult.value.hash;
|
|
3229
|
+
if (toResult.value.provenance.kind === "ref") {
|
|
3230
|
+
const refEntry = allRefs[toResult.value.provenance.refName];
|
|
3231
|
+
if (refEntry) refInvariants = refEntry.invariants;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
options.onPreflightComplete?.({
|
|
3235
|
+
configPath,
|
|
3236
|
+
migrationsRelative,
|
|
3237
|
+
dbConnection,
|
|
3238
|
+
hasExplicitFrom
|
|
3239
|
+
});
|
|
3240
|
+
const markerBySpace = /* @__PURE__ */ new Map();
|
|
3241
|
+
const allSpaces = [aggregate.app, ...aggregate.extensions];
|
|
3242
|
+
if (hasExplicitFrom) if (options.from === "@db") {
|
|
3243
|
+
const missingDb = requireLiveDatabase({
|
|
3244
|
+
dbConnection,
|
|
3245
|
+
hasDriver,
|
|
3246
|
+
why: "@db resolves to the live database marker and requires a --db connection",
|
|
3247
|
+
retryCommand: "prisma-cli migrate --show --from @db --db $DATABASE_URL"
|
|
3248
|
+
});
|
|
3249
|
+
if (missingDb) return notOk(missingDb);
|
|
3250
|
+
} else {
|
|
3251
|
+
const fromResult = parseContractRef(options.from, {
|
|
3252
|
+
graph: appGraph,
|
|
3253
|
+
refs: allRefs,
|
|
3254
|
+
contractHash
|
|
3255
|
+
});
|
|
3256
|
+
if (!fromResult.ok) return notOk(mapRefResolutionError(fromResult.failure));
|
|
3257
|
+
if (fromResult.value.provenance.kind === "reserved-db") {
|
|
3258
|
+
const missingDb = requireLiveDatabase({
|
|
3259
|
+
dbConnection,
|
|
3260
|
+
hasDriver,
|
|
3261
|
+
why: "@db resolves to the live database marker and requires a --db connection"
|
|
3262
|
+
});
|
|
3263
|
+
if (missingDb) return notOk(missingDb);
|
|
3264
|
+
} else {
|
|
3265
|
+
const fromHash = fromResult.value.hash;
|
|
3266
|
+
const offlineMarker = fromHash === "empty" ? null : {
|
|
3267
|
+
storageHash: fromHash,
|
|
3268
|
+
invariants: []
|
|
3269
|
+
};
|
|
3270
|
+
markerBySpace.set(aggregate.app.spaceId, offlineMarker);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
const needsLiveMarker = !hasExplicitFrom || options.from === "@db";
|
|
3274
|
+
if (needsLiveMarker) {
|
|
3275
|
+
if (!dbConnection || !hasDriver) return notOk(errorDatabaseConnectionRequired({
|
|
3276
|
+
why: "A database connection is required to read the live marker for migrate --show",
|
|
3277
|
+
commandName: "migrate --show"
|
|
3278
|
+
}));
|
|
3279
|
+
const client = (options.createClient ?? createControlClient)({
|
|
3280
|
+
family: config.family,
|
|
3281
|
+
target: config.target,
|
|
3282
|
+
adapter: config.adapter,
|
|
3283
|
+
driver: config.driver,
|
|
3284
|
+
extensions: config.extensions ?? []
|
|
3285
|
+
});
|
|
3286
|
+
try {
|
|
3287
|
+
await client.connect(dbConnection);
|
|
3288
|
+
const allMarkers = await client.readAllMarkers();
|
|
3289
|
+
for (const space of allSpaces) {
|
|
3290
|
+
const marker = allMarkers.get(space.spaceId);
|
|
3291
|
+
markerBySpace.set(space.spaceId, marker ?? null);
|
|
3292
|
+
}
|
|
3293
|
+
} catch (error) {
|
|
3294
|
+
if (CliStructuredError$1.is(error)) return notOk(error);
|
|
3295
|
+
return notOk(errorUnexpected(error instanceof Error ? error.message : String(error), { why: `Failed to read live DB marker: ${error instanceof Error ? error.message : String(error)}` }));
|
|
3296
|
+
} finally {
|
|
3297
|
+
await closeQuietly(client);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
const canonicalOrderSpaces = [...aggregate.extensions, aggregate.app];
|
|
3301
|
+
const orderedMigrations = [];
|
|
3302
|
+
for (const space of canonicalOrderSpaces) {
|
|
3303
|
+
const isAppSpace = space.spaceId === aggregate.app.spaceId;
|
|
3304
|
+
const headRef = requireHeadRef(space);
|
|
3305
|
+
const outcome = planSpacePath({
|
|
3306
|
+
space,
|
|
3307
|
+
aggregate,
|
|
3308
|
+
targetHash: isAppSpace ? targetHash : headRef.hash,
|
|
3309
|
+
refInvariants: isAppSpace ? refInvariants : void 0,
|
|
3310
|
+
liveMarker: markerBySpace.get(space.spaceId) ?? null
|
|
3311
|
+
});
|
|
3312
|
+
if (outcome.kind === "at-head") continue;
|
|
3313
|
+
if (outcome.kind === "never-planned") return notOk(errorPathUnreachable({
|
|
3314
|
+
code: "MIGRATION_PATH_NOT_FOUND",
|
|
3315
|
+
summary: `No on-disk migrations for contract space "${outcome.spaceId}"`,
|
|
3316
|
+
why: `migrate is replay-only: space "${outcome.spaceId}" has no on-disk migrations but its head ref targets "${outcome.targetHash}".`,
|
|
3317
|
+
meta: {
|
|
3318
|
+
spaceId: outcome.spaceId,
|
|
3319
|
+
target: outcome.targetHash,
|
|
3320
|
+
kind: "neverPlanned"
|
|
3321
|
+
}
|
|
3322
|
+
}));
|
|
3323
|
+
if (outcome.kind === "unreachable") {
|
|
3324
|
+
const fromHash = outcome.liveMarker?.storageHash ?? "empty";
|
|
3325
|
+
return notOk(errorPathUnreachable({
|
|
3326
|
+
code: "MIGRATION_PATH_NOT_FOUND",
|
|
3327
|
+
summary: `No migration path from ${fromHash.slice(0, 14)} to ${outcome.targetHash.slice(0, 14)} in space "${outcome.spaceId}".`,
|
|
3328
|
+
why: `The migration graph has no path from the from-state to the target in space "${outcome.spaceId}".`,
|
|
3329
|
+
meta: {
|
|
3330
|
+
spaceId: outcome.spaceId,
|
|
3331
|
+
from: fromHash,
|
|
3332
|
+
to: outcome.targetHash
|
|
3333
|
+
}
|
|
3334
|
+
}));
|
|
3335
|
+
}
|
|
3336
|
+
if (outcome.kind === "unsatisfiable") return notOk(errorRuntime("MIGRATION.NO_INVARIANT_PATH", `Missing required invariants for space "${outcome.spaceId}"`, {
|
|
3337
|
+
why: `The path requires invariants not available on disk: ${outcome.missing.join(", ")}`,
|
|
3338
|
+
fix: "Add a migration on the path that runs `dataTransform({ invariantId: \"<id>\", … })` for each missing invariant, or retarget the ref to a hash whose path already provides them.",
|
|
3339
|
+
meta: {
|
|
3340
|
+
spaceId: outcome.spaceId,
|
|
3341
|
+
missing: [...outcome.missing]
|
|
3342
|
+
}
|
|
3343
|
+
}));
|
|
3344
|
+
for (const edge of outcome.plan.migrationEdges) orderedMigrations.push({
|
|
3345
|
+
spaceId: space.spaceId,
|
|
3346
|
+
dirName: edge.dirName,
|
|
3347
|
+
migrationHash: edge.migrationHash,
|
|
3348
|
+
from: edge.from,
|
|
3349
|
+
to: edge.to
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
const count = orderedMigrations.length;
|
|
3353
|
+
return ok({
|
|
3354
|
+
aggregate,
|
|
3355
|
+
contractHash,
|
|
3356
|
+
migrations: orderedMigrations,
|
|
3357
|
+
summary: count === 0 ? "Already up to date — nothing to run" : `${count} migration${count === 1 ? "" : "s"} will run`,
|
|
3358
|
+
renderMarkerHashBySpace: new Map(allSpaces.map((s) => [s.spaceId, markerBySpace.get(s.spaceId)?.storageHash ?? "empty"])),
|
|
3359
|
+
usedLiveMarker: needsLiveMarker
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
function resolveContractRef$1(input, context) {
|
|
3363
|
+
const result = parseContractRef(input, context);
|
|
3364
|
+
return result.ok ? ok(result.value) : notOk(mapRefResolutionError(result.failure));
|
|
3365
|
+
}
|
|
3366
|
+
function resolveMigrationRef(input, context) {
|
|
3367
|
+
const result = parseMigrationRef(input, context);
|
|
3368
|
+
return result.ok ? ok(result.value) : notOk(mapRefResolutionError(result.failure));
|
|
3369
|
+
}
|
|
3370
|
+
function compareSpaceIds(a, b) {
|
|
3371
|
+
if (a === APP_SPACE_ID$1) return b === APP_SPACE_ID$1 ? 0 : -1;
|
|
3372
|
+
if (b === APP_SPACE_ID$1) return 1;
|
|
3373
|
+
if (a < b) return -1;
|
|
3374
|
+
if (a > b) return 1;
|
|
3375
|
+
return 0;
|
|
3376
|
+
}
|
|
3377
|
+
function compareDirNamesDescending(a, b) {
|
|
3378
|
+
if (a.name < b.name) return 1;
|
|
3379
|
+
if (a.name > b.name) return -1;
|
|
3380
|
+
return 0;
|
|
3381
|
+
}
|
|
3382
|
+
/**
|
|
3383
|
+
* Ref names decorating a space's destination contract hashes. The
|
|
3384
|
+
* tolerant `space.refs` deliberately omits the structural `head.json`;
|
|
3385
|
+
* for extension spaces the old enumerator surfaced it as a `head`
|
|
3386
|
+
* decoration on the tip migration, so fold `space.headRef` back in to
|
|
3387
|
+
* keep that output. The app space synthesises its head, so it carries
|
|
3388
|
+
* no on-disk `head` ref to restore.
|
|
3389
|
+
*/
|
|
3390
|
+
function listRefsByContractHash(space) {
|
|
3391
|
+
const byHash = new Map(refsByContractHash(space.refs));
|
|
3392
|
+
if (space.spaceId !== APP_SPACE_ID$1 && space.headRef !== null) {
|
|
3393
|
+
const hash = space.headRef.hash;
|
|
3394
|
+
const bucket = byHash.get(hash) ?? [];
|
|
3395
|
+
if (!bucket.includes("head")) byHash.set(hash, [...bucket, HEAD_REF_NAME].sort());
|
|
3396
|
+
}
|
|
3397
|
+
return byHash;
|
|
3398
|
+
}
|
|
3399
|
+
async function orderedOnDiskSpaceIds(projectMigrationsDir) {
|
|
3400
|
+
return (await listContractSpaceDirectories(projectMigrationsDir)).filter(isValidSpaceId).sort(compareSpaceIds);
|
|
3401
|
+
}
|
|
3402
|
+
/**
|
|
3403
|
+
* Project the loaded {@link ContractSpaceAggregate} into the render-ready
|
|
3404
|
+
* {@link MigrationSpaceListEntry} rows `migration list` displays.
|
|
3405
|
+
*
|
|
3406
|
+
* Space membership matches the on-disk contract-space directories (not the
|
|
3407
|
+
* aggregate's always-present synthesized app space when `migrations/app/`
|
|
3408
|
+
* is absent); package and ref data come from `aggregate.space(id)`.
|
|
3409
|
+
*/
|
|
3410
|
+
async function migrationSpaceListEntriesFromAggregate(aggregate, projectMigrationsDir) {
|
|
3411
|
+
const spaceIds = await orderedOnDiskSpaceIds(projectMigrationsDir);
|
|
3412
|
+
const spaces = [];
|
|
3413
|
+
for (const spaceId of spaceIds) {
|
|
3414
|
+
const space = aggregate.space(spaceId);
|
|
3415
|
+
if (space === void 0) continue;
|
|
3416
|
+
const refsByHash = listRefsByContractHash(space);
|
|
3417
|
+
const migrations = space.packages.map((pkg) => ({
|
|
3418
|
+
name: pkg.dirName,
|
|
3419
|
+
hash: pkg.metadata.migrationHash,
|
|
3420
|
+
fromContract: pkg.metadata.from,
|
|
3421
|
+
toContract: pkg.metadata.to,
|
|
3422
|
+
operationCount: pkg.ops.length,
|
|
3423
|
+
createdAt: pkg.metadata.createdAt,
|
|
3424
|
+
refs: [...refsByHash.get(pkg.metadata.to) ?? []],
|
|
3425
|
+
providedInvariants: [...pkg.metadata.providedInvariants]
|
|
3426
|
+
})).sort(compareDirNamesDescending);
|
|
3427
|
+
spaces.push({
|
|
3428
|
+
space: spaceId,
|
|
3429
|
+
migrations
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
return spaces;
|
|
3433
|
+
}
|
|
3434
|
+
function computeSummary(spaces) {
|
|
3435
|
+
const totalMigrations = spaces.reduce((count, space) => count + space.migrations.length, 0);
|
|
3436
|
+
if (spaces.length <= 1) return `${totalMigrations} migration(s) on disk`;
|
|
3437
|
+
return `${totalMigrations} migration(s) across ${spaces.length} contract space(s)`;
|
|
3438
|
+
}
|
|
3439
|
+
/**
|
|
3440
|
+
* Policy core of `migration list`: validates `--space`, narrows the
|
|
3441
|
+
* pre-enumerated spaces, and assembles a {@link MigrationListResult}.
|
|
3442
|
+
*
|
|
3443
|
+
* - `migrations/` missing or contains no valid space directories →
|
|
3444
|
+
* caller passes `spaces: []`; this synthesizes `[{ spaceId: APP_SPACE_ID, migrations: [] }]`.
|
|
3445
|
+
* - `--space <id>` on an existing-but-empty space → `{ spaceId, migrations: [] }` in the input.
|
|
3446
|
+
* - `--space <id>` on a non-existent (or reserved) space → `SPACE_NOT_FOUND`.
|
|
3447
|
+
*/
|
|
3448
|
+
function runMigrationList(inputs) {
|
|
3449
|
+
const { spaces, spaceFilter } = inputs;
|
|
3450
|
+
if (spaceFilter !== void 0 && !isValidSpaceId(spaceFilter)) return notOk(errorInvalidSpaceId(spaceFilter));
|
|
3451
|
+
if (spaceFilter !== void 0 && !spaces.some((s) => s.space === spaceFilter)) return notOk(errorSpaceNotFound(spaceFilter, spaces.map((s) => s.space).sort()));
|
|
3452
|
+
const scopedSpaces = spaceFilter !== void 0 ? spaces.filter((s) => s.space === spaceFilter) : spaces;
|
|
3453
|
+
const resultSpaces = scopedSpaces.length === 0 ? [{
|
|
3454
|
+
space: APP_SPACE_ID$1,
|
|
3455
|
+
migrations: []
|
|
3456
|
+
}] : scopedSpaces;
|
|
3457
|
+
return ok({
|
|
3458
|
+
ok: true,
|
|
3459
|
+
spaces: [...resultSpaces],
|
|
3460
|
+
summary: computeSummary(resultSpaces)
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
function looksLikePath(target) {
|
|
3464
|
+
return target.includes("/") || target.includes("\\");
|
|
3465
|
+
}
|
|
3466
|
+
function resolveAppTargetPath(cwd, target, appMigrationsDir, appMigrationsRelative) {
|
|
3467
|
+
const targetPath = resolve(cwd, target);
|
|
3468
|
+
const relativeToApp = relative(appMigrationsDir, targetPath);
|
|
3469
|
+
if (relativeToApp === "" || relativeToApp === "." || relativeToApp.startsWith("..") || isAbsolute(relativeToApp)) return notOk(errorRuntime("MIGRATION.TARGET_NOT_APP_SPACE", "Target must point to an app-space migration", {
|
|
3470
|
+
why: `Expected a path under ${appMigrationsRelative}, got ${target}`,
|
|
3471
|
+
fix: "Pass an app-space migration directory or use a hash prefix."
|
|
3472
|
+
}));
|
|
3473
|
+
return ok(targetPath);
|
|
3474
|
+
}
|
|
3475
|
+
/**
|
|
3476
|
+
* Resolve a filesystem-path target to the migration dir that contains it,
|
|
3477
|
+
* searching each in-scope space's `migrationsDir`. A path is explicit, so
|
|
3478
|
+
* it can belong to at most one space — returns the first match, or `null`
|
|
3479
|
+
* when the path falls outside every space dir.
|
|
3480
|
+
*/
|
|
3481
|
+
function resolveTargetPathAcrossSpaces(cwd, target, spaces) {
|
|
3482
|
+
const targetPath = resolve(cwd, target);
|
|
3483
|
+
for (const space of spaces) {
|
|
3484
|
+
const rel = relative(space.migrationsDir, targetPath);
|
|
3485
|
+
if (!(rel === "" || rel === "." || rel.startsWith("..") || isAbsolute(rel))) return targetPath;
|
|
3486
|
+
}
|
|
3487
|
+
return null;
|
|
3488
|
+
}
|
|
3489
|
+
function findPackageByDirPath(packages, resolvedDirPath) {
|
|
3490
|
+
const normalized = resolve(resolvedDirPath);
|
|
3491
|
+
return packages.find((p) => resolve(p.dirPath) === normalized);
|
|
3492
|
+
}
|
|
3493
|
+
/**
|
|
3494
|
+
* Policy core of `migration check`: per-space explicit graph checks, aggregate-integrity loading, and single-target resolution.
|
|
3495
|
+
*/
|
|
3496
|
+
function migrationPathRelative(cwd, dirPath) {
|
|
3497
|
+
return relative(cwd, dirPath);
|
|
3498
|
+
}
|
|
3499
|
+
function migrationFileRelative(cwd, dirPath, fileName) {
|
|
3500
|
+
return join(migrationPathRelative(cwd, dirPath), fileName);
|
|
3501
|
+
}
|
|
3502
|
+
function checkFileExists(space, dirPath, dirName, fileName) {
|
|
3503
|
+
if (!existsSync(join(dirPath, fileName))) return {
|
|
3504
|
+
space: space.spaceId,
|
|
3505
|
+
code: "MIGRATION.CHECK_FILE_MISSING",
|
|
3506
|
+
where: migrationFileRelative(space.cwd, dirPath, fileName),
|
|
3507
|
+
why: `${fileName} is missing from ${dirName}`,
|
|
3508
|
+
nextActions: [chooseAction("Re-emit the migration package, or restore it from version control")]
|
|
3509
|
+
};
|
|
3510
|
+
return null;
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Snapshot-store consistency check for one migration package (D6.5):
|
|
3514
|
+
* absent store entry is not an issue (runner independence, ADR 199 — a
|
|
3515
|
+
* package dir with only `migration.json` + `ops.json` is legitimate); a
|
|
3516
|
+
* present entry whose inner `storage.storageHash` disagrees with
|
|
3517
|
+
* `pkg.metadata.to` is `MIGRATION.CHECK_SNAPSHOT_HASH_MISMATCH`; an unparseable store
|
|
3518
|
+
* entry (or a malformed `to`) is `MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE`.
|
|
3519
|
+
*/
|
|
3520
|
+
async function checkSnapshotConsistency(space, pkg, migrationsDir) {
|
|
3521
|
+
const spaceId = space.spaceId;
|
|
3522
|
+
let snapshotDir;
|
|
3523
|
+
try {
|
|
3524
|
+
snapshotDir = contractSnapshotDir(migrationsDir, pkg.metadata.to);
|
|
3525
|
+
} catch {
|
|
3526
|
+
return {
|
|
3527
|
+
space: spaceId,
|
|
3528
|
+
code: "MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE",
|
|
3529
|
+
where: migrationPathRelative(space.cwd, pkg.dirPath),
|
|
3530
|
+
why: `Migration "${pkg.dirName}" declares to="${pkg.metadata.to}", which is not a well-formed contract snapshot hash.`,
|
|
3531
|
+
nextActions: [chooseAction("Re-emit the migration package so migration.json declares a valid 64-hex to-hash")]
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
let raw;
|
|
3535
|
+
try {
|
|
3536
|
+
raw = await readContractSnapshotJson(migrationsDir, pkg.metadata.to);
|
|
3537
|
+
} catch (error) {
|
|
3538
|
+
if (MigrationToolsError.is(error) && error.code === "MIGRATION.CONTRACT_SNAPSHOT_MISSING") return null;
|
|
3539
|
+
return {
|
|
3540
|
+
space: spaceId,
|
|
3541
|
+
code: "MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE",
|
|
3542
|
+
where: migrationPathRelative(space.cwd, pkg.dirPath),
|
|
3543
|
+
why: `Migration "${pkg.dirName}" has an unparseable contract snapshot at ${snapshotDir}/contract.json.`,
|
|
3544
|
+
nextActions: [chooseAction("Restore migrations/snapshots/ from version control"), chooseAction("Or re-run the command that produced this migration to regenerate its snapshot")]
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
const snapshotHash = (raw !== null && typeof raw === "object" ? raw : {})["storage"]?.["storageHash"];
|
|
3548
|
+
if (typeof snapshotHash === "string" && snapshotHash !== pkg.metadata.to) return {
|
|
3549
|
+
space: spaceId,
|
|
3550
|
+
code: "MIGRATION.CHECK_SNAPSHOT_HASH_MISMATCH",
|
|
3551
|
+
where: migrationPathRelative(space.cwd, pkg.dirPath),
|
|
3552
|
+
why: `Migration "${pkg.dirName}" declares to=${pkg.metadata.to} but its contract snapshot has storageHash=${snapshotHash}`,
|
|
3553
|
+
nextActions: [chooseAction("Re-emit the migration package so migration.json and its contract snapshot agree")]
|
|
3554
|
+
};
|
|
3555
|
+
return null;
|
|
3556
|
+
}
|
|
3557
|
+
/**
|
|
3558
|
+
* Project the loaded {@link ContractSpaceAggregate} into the
|
|
3559
|
+
* {@link CheckSpace} rows the multi-space check iterates — one per on-disk
|
|
3560
|
+
* contract-space directory, in the aggregate's `app`-first ordering. Mirrors
|
|
3561
|
+
* `migration list`'s `migrationSpaceListEntriesFromAggregate`: space
|
|
3562
|
+
* membership matches the on-disk directories, package / ref / graph data come
|
|
3563
|
+
* from `aggregate.space(id)`.
|
|
3564
|
+
*/
|
|
3565
|
+
async function enumerateCheckSpaces(aggregate, projectMigrationsDir, cwd) {
|
|
3566
|
+
const candidateDirs = await listContractSpaceDirectories(projectMigrationsDir);
|
|
3567
|
+
const onDiskSpaceIds = new Set(candidateDirs.filter(isValidSpaceId));
|
|
3568
|
+
const spaces = [];
|
|
3569
|
+
for (const space of aggregate.spaces()) {
|
|
3570
|
+
const spaceId = space.spaceId;
|
|
3571
|
+
if (!isValidSpaceId(spaceId)) continue;
|
|
3572
|
+
if (!onDiskSpaceIds.has(spaceId)) continue;
|
|
3573
|
+
const migrationsDir = spaceMigrationDirectory(projectMigrationsDir, spaceId);
|
|
3574
|
+
spaces.push({
|
|
3575
|
+
spaceId,
|
|
3576
|
+
packages: space.packages,
|
|
3577
|
+
refs: space.refs,
|
|
3578
|
+
graph: space.graph(),
|
|
3579
|
+
migrationsDir,
|
|
3580
|
+
refsDir: spaceRefsDirectory(migrationsDir),
|
|
3581
|
+
projectMigrationsDir,
|
|
3582
|
+
cwd
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
return spaces;
|
|
3586
|
+
}
|
|
3587
|
+
function checkManifestFilesPresent(space) {
|
|
3588
|
+
if (!existsSync(space.migrationsDir)) return [];
|
|
3589
|
+
const loadedDirNames = new Set(space.packages.map((p) => p.dirName));
|
|
3590
|
+
const failures = [];
|
|
3591
|
+
let entries;
|
|
3592
|
+
try {
|
|
3593
|
+
entries = readdirSync(space.migrationsDir);
|
|
3594
|
+
} catch {
|
|
3595
|
+
return failures;
|
|
3596
|
+
}
|
|
3597
|
+
for (const entry of entries) {
|
|
3598
|
+
if (entry.startsWith(".") || entry.startsWith("_") || entry === "refs") continue;
|
|
3599
|
+
const entryPath = join(space.migrationsDir, entry);
|
|
3600
|
+
try {
|
|
3601
|
+
if (!statSync(entryPath).isDirectory()) continue;
|
|
3602
|
+
} catch {
|
|
3603
|
+
continue;
|
|
3604
|
+
}
|
|
3605
|
+
if (!loadedDirNames.has(entry)) for (const f of ["migration.json", "ops.json"]) {
|
|
3606
|
+
const fail = checkFileExists(space, entryPath, entry, f);
|
|
3607
|
+
if (fail) failures.push(fail);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
return failures;
|
|
3611
|
+
}
|
|
3612
|
+
function checkReachability(space) {
|
|
3613
|
+
const allToHashes = new Set(space.packages.map((p) => p.metadata.to));
|
|
3614
|
+
const failures = [];
|
|
3615
|
+
for (const pkg of space.packages) if (!(pkg.metadata.from === null || allToHashes.has(pkg.metadata.from) || pkg.metadata.from === "empty")) failures.push({
|
|
3616
|
+
space: space.spaceId,
|
|
3617
|
+
code: "MIGRATION.CHECK_UNREACHABLE_MIGRATION",
|
|
3618
|
+
where: migrationPathRelative(space.cwd, pkg.dirPath),
|
|
3619
|
+
why: `Migration "${pkg.dirName}" starts from ${pkg.metadata.from} which no other migration produces`,
|
|
3620
|
+
nextActions: [chooseAction("Delete the unreachable migration"), chooseAction("Or re-emit a migration that connects it to the graph")]
|
|
3621
|
+
});
|
|
3622
|
+
return failures;
|
|
3623
|
+
}
|
|
3624
|
+
function checkDanglingRefs(space) {
|
|
3625
|
+
const failures = [];
|
|
3626
|
+
for (const [name, entry] of Object.entries(space.refs)) if (!space.graph.nodes.has(entry.hash)) failures.push({
|
|
3627
|
+
space: space.spaceId,
|
|
3628
|
+
code: "MIGRATION.CHECK_DANGLING_REF",
|
|
3629
|
+
where: relative(space.cwd, join(space.refsDir, `${name}.json`)),
|
|
3630
|
+
why: `Ref "${name}" points at ${entry.hash} which does not exist in the migration graph`,
|
|
3631
|
+
nextActions: [runCommandAction("Point the ref at a graph node", `prisma-cli ref set ${name} <valid-hash>`), chooseAction("Or delete the ref")]
|
|
3632
|
+
});
|
|
3633
|
+
return failures;
|
|
3634
|
+
}
|
|
3635
|
+
async function checkSpace(space) {
|
|
3636
|
+
const snapshotFailures = await Promise.all(space.packages.map((pkg) => checkSnapshotConsistency(space, pkg, space.projectMigrationsDir)));
|
|
3637
|
+
return [
|
|
3638
|
+
...checkManifestFilesPresent(space),
|
|
3639
|
+
...snapshotFailures.filter((f) => f !== null),
|
|
3640
|
+
...checkReachability(space),
|
|
3641
|
+
...checkDanglingRefs(space)
|
|
3642
|
+
];
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Policy core of the holistic `migration check`: validates `--space`,
|
|
3646
|
+
* narrows the pre-enumerated spaces, and runs the per-space explicit graph
|
|
3647
|
+
* checks (file-existence, snapshot consistency, reachability, dangling
|
|
3648
|
+
* refs), aggregating every failure into one {@link MigrationCheckResult}.
|
|
3649
|
+
*
|
|
3650
|
+
* `--space` validation mirrors `migration list`: an invalid id →
|
|
3651
|
+
* {@link errorInvalidSpaceId}; an id with no on-disk space →
|
|
3652
|
+
* {@link errorSpaceNotFound}. Both map to exit `PRECONDITION` at the shell.
|
|
3653
|
+
* Aggregate-integrity violations (which already span every space) are folded
|
|
3654
|
+
* in by the caller, not here.
|
|
3655
|
+
*/
|
|
3656
|
+
async function runMigrationCheck(inputs) {
|
|
3657
|
+
const { spaces, spaceFilter } = inputs;
|
|
3658
|
+
if (spaceFilter !== void 0 && !isValidSpaceId(spaceFilter)) return notOk(errorInvalidSpaceId(spaceFilter));
|
|
3659
|
+
if (spaceFilter !== void 0 && !spaces.some((s) => s.spaceId === spaceFilter)) return notOk(errorSpaceNotFound(spaceFilter, spaces.map((s) => s.spaceId).sort()));
|
|
3660
|
+
const scopedSpaces = spaceFilter !== void 0 ? spaces.filter((s) => s.spaceId === spaceFilter) : spaces;
|
|
3661
|
+
const failures = (await Promise.all(scopedSpaces.map(checkSpace))).flat();
|
|
3662
|
+
if (failures.length === 0) return ok({
|
|
3663
|
+
ok: true,
|
|
3664
|
+
failures: [],
|
|
3665
|
+
summary: "All checks passed"
|
|
3666
|
+
});
|
|
3667
|
+
return ok({
|
|
3668
|
+
ok: false,
|
|
3669
|
+
failures,
|
|
3670
|
+
summary: `${failures.length} integrity failure(s)`
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3673
|
+
async function loadAggregateIntegrityViolations(config, migrationsDir) {
|
|
3674
|
+
try {
|
|
3675
|
+
const contractJsonContent = await readFile(resolveContractPath(config), "utf-8");
|
|
3676
|
+
const familyInstance = config.family.create(createControlStack(config));
|
|
3677
|
+
const declaredExtensions = toDeclaredExtensionsFromRaw(config.extensions ?? []);
|
|
3678
|
+
const parsedAppContract = JSON.parse(contractJsonContent);
|
|
3679
|
+
return (await loadContractSpaceAggregate({
|
|
3680
|
+
migrationsDir,
|
|
3681
|
+
deserializeContract: (json) => familyInstance.deserializeContract(json),
|
|
3682
|
+
appContract: familyInstance.deserializeContract(parsedAppContract)
|
|
3683
|
+
})).checkIntegrity({
|
|
3684
|
+
declaredExtensions,
|
|
3685
|
+
checkContracts: true
|
|
3686
|
+
});
|
|
3687
|
+
} catch {
|
|
3688
|
+
return [];
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
/**
|
|
3692
|
+
* Ranks ref-resolution failure kinds by how informative they are, so a
|
|
3693
|
+
* single-target check surfaces the most useful failure across spaces instead of
|
|
3694
|
+
* whichever space failed first. `not-found` (the input matched nothing here)
|
|
3695
|
+
* says the least; a malformed input, a wrong grammar, or an in-space ambiguity
|
|
3696
|
+
* all say more.
|
|
3697
|
+
*/
|
|
3698
|
+
function refFailureSpecificity(error) {
|
|
3699
|
+
switch (error.kind) {
|
|
3700
|
+
case "wrong-grammar": return 3;
|
|
3701
|
+
case "ambiguous": return 2;
|
|
3702
|
+
case "invalid-format": return 1;
|
|
3703
|
+
case "not-found": return 0;
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
/**
|
|
3707
|
+
* Single-target (`check <ref/path>`) mode — resolves a migration reference
|
|
3708
|
+
* across all contract spaces (or the one space narrowed by `--space <id>`).
|
|
3709
|
+
*
|
|
3710
|
+
* Resolution:
|
|
3711
|
+
* - filesystem path → find the owning space by checking which space's
|
|
3712
|
+
* `migrationsDir` contains the resolved path; falls back to app-relative
|
|
3713
|
+
* validation when the path is outside every space dir.
|
|
3714
|
+
* - ref → `parseMigrationRef` against each in-scope space; collect every
|
|
3715
|
+
* (space, package) hit; 0 hits = not-found, 1 = check it, >1 = ambiguity
|
|
3716
|
+
* error (qualify with `--space`).
|
|
3717
|
+
*
|
|
3718
|
+
* `--space <id>` is validated the same way the holistic path does it:
|
|
3719
|
+
* invalid id → `errorInvalidSpaceId`; no on-disk space → `errorSpaceNotFound`.
|
|
3720
|
+
*/
|
|
3721
|
+
async function checkSingleTarget(target, inputs) {
|
|
3722
|
+
const { spaces, spaceFilter, appMigrationsDir, appMigrationsRelative, cwd } = inputs;
|
|
3723
|
+
if (spaceFilter !== void 0 && !isValidSpaceId(spaceFilter)) return {
|
|
3724
|
+
error: errorInvalidSpaceId(spaceFilter),
|
|
3725
|
+
exitCode: 2
|
|
3726
|
+
};
|
|
3727
|
+
if (spaceFilter !== void 0 && !spaces.some((s) => s.spaceId === spaceFilter)) return {
|
|
3728
|
+
error: errorSpaceNotFound(spaceFilter, spaces.map((s) => s.spaceId).sort()),
|
|
3729
|
+
exitCode: 2
|
|
3730
|
+
};
|
|
3731
|
+
const scopedSpaces = spaceFilter !== void 0 ? spaces.filter((s) => s.spaceId === spaceFilter) : spaces;
|
|
3732
|
+
let matchedSpace;
|
|
3733
|
+
let matchedPkg;
|
|
3734
|
+
if (looksLikePath(target)) {
|
|
3735
|
+
const resolvedPath = resolveTargetPathAcrossSpaces(cwd, target, scopedSpaces);
|
|
3736
|
+
if (resolvedPath !== null) for (const space of scopedSpaces) {
|
|
3737
|
+
const found = findPackageByDirPath(space.packages, resolvedPath);
|
|
3738
|
+
if (found) {
|
|
3739
|
+
matchedSpace = space;
|
|
3740
|
+
matchedPkg = found;
|
|
3741
|
+
break;
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
else {
|
|
3745
|
+
const resolved = resolveAppTargetPath(cwd, target, appMigrationsDir, appMigrationsRelative);
|
|
3746
|
+
if (!resolved.ok) return {
|
|
3747
|
+
error: resolved.failure,
|
|
3748
|
+
exitCode: 2
|
|
3749
|
+
};
|
|
3750
|
+
const appSpace = scopedSpaces.find((s) => s.spaceId === "app");
|
|
3751
|
+
if (appSpace) {
|
|
3752
|
+
matchedSpace = appSpace;
|
|
3753
|
+
matchedPkg = findPackageByDirPath(appSpace.packages, resolved.value);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
} else {
|
|
3757
|
+
const hits = [];
|
|
3758
|
+
let bestParseFailure;
|
|
3759
|
+
for (const space of scopedSpaces) {
|
|
3760
|
+
const migResult = parseMigrationRef(target, {
|
|
3761
|
+
graph: space.graph,
|
|
3762
|
+
refs: space.refs
|
|
3763
|
+
});
|
|
3764
|
+
if (!migResult.ok) {
|
|
3765
|
+
if (bestParseFailure === void 0 || refFailureSpecificity(migResult.failure) > refFailureSpecificity(bestParseFailure)) bestParseFailure = migResult.failure;
|
|
3766
|
+
continue;
|
|
3767
|
+
}
|
|
3768
|
+
const pkg = space.packages.find((p) => p.metadata.migrationHash === migResult.value.migrationHash);
|
|
3769
|
+
if (pkg) hits.push({
|
|
3770
|
+
space,
|
|
3771
|
+
pkg
|
|
3772
|
+
});
|
|
3773
|
+
}
|
|
3774
|
+
if (hits.length > 1) return {
|
|
3775
|
+
error: errorAmbiguousMigrationRef(target, hits.map((h) => h.space.spaceId)),
|
|
3776
|
+
exitCode: 2
|
|
3777
|
+
};
|
|
3778
|
+
if (hits.length === 1) {
|
|
3779
|
+
matchedSpace = hits[0].space;
|
|
3780
|
+
matchedPkg = hits[0].pkg;
|
|
3781
|
+
} else if (bestParseFailure !== void 0) return {
|
|
3782
|
+
error: mapRefResolutionError(bestParseFailure),
|
|
3783
|
+
exitCode: 2
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
if (!matchedPkg || !matchedSpace) return {
|
|
3787
|
+
result: {
|
|
3788
|
+
ok: false,
|
|
3789
|
+
failures: [],
|
|
3790
|
+
summary: `Migration package for "${target}" not found on disk`
|
|
3791
|
+
},
|
|
3792
|
+
exitCode: 2
|
|
3793
|
+
};
|
|
3794
|
+
const failures = [...checkManifestFilesPresent(matchedSpace)];
|
|
3795
|
+
for (const f of ["migration.json", "ops.json"]) {
|
|
3796
|
+
const fail = checkFileExists(matchedSpace, matchedPkg.dirPath, matchedPkg.dirName, f);
|
|
3797
|
+
if (fail) failures.push(fail);
|
|
3798
|
+
}
|
|
3799
|
+
const verification = verifyMigrationHash(matchedPkg);
|
|
3800
|
+
if (!verification.ok) failures.push({
|
|
3801
|
+
space: matchedSpace.spaceId,
|
|
3802
|
+
code: "MIGRATION.CHECK_HASH_MISMATCH",
|
|
3803
|
+
where: migrationFileRelative(matchedSpace.cwd, matchedPkg.dirPath, "migration.json"),
|
|
3804
|
+
why: `Stored hash ${verification.storedHash} does not match recomputed hash ${verification.computedHash}`,
|
|
3805
|
+
nextActions: [chooseAction("Re-emit the migration package, or restore it from version control")]
|
|
3806
|
+
});
|
|
3807
|
+
const snapshotFailure = await checkSnapshotConsistency(matchedSpace, matchedPkg, matchedSpace.projectMigrationsDir);
|
|
3808
|
+
if (snapshotFailure) failures.push(snapshotFailure);
|
|
3809
|
+
const resolvedSpaceId = matchedSpace.spaceId !== "app" ? matchedSpace.spaceId : void 0;
|
|
3810
|
+
if (failures.length === 0) return {
|
|
3811
|
+
result: {
|
|
3812
|
+
ok: true,
|
|
3813
|
+
failures: [],
|
|
3814
|
+
summary: "All checks passed"
|
|
3815
|
+
},
|
|
3816
|
+
exitCode: 0,
|
|
3817
|
+
...ifDefined("resolvedSpaceId", resolvedSpaceId)
|
|
3818
|
+
};
|
|
3819
|
+
return {
|
|
3820
|
+
result: {
|
|
3821
|
+
ok: false,
|
|
3822
|
+
failures,
|
|
3823
|
+
summary: `${failures.length} integrity failure(s)`
|
|
3824
|
+
},
|
|
3825
|
+
exitCode: 4,
|
|
3826
|
+
...ifDefined("resolvedSpaceId", resolvedSpaceId)
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
/**
|
|
3830
|
+
* Project scoped list entries into the per-space contracts+migrations rows the migration
|
|
3831
|
+
* graph --json output serializes (EMPTY_CONTRACT_HASH from-hash → null). Skips space ids
|
|
3832
|
+
* the aggregate no longer resolves, mirroring the command loop.
|
|
3833
|
+
*/
|
|
3834
|
+
function buildMigrationSpaceGraphEntries(args) {
|
|
3835
|
+
const spaces = [];
|
|
3836
|
+
for (const spaceEntry of args.scopedSpaces) {
|
|
3837
|
+
const space = args.aggregate.space(spaceEntry.space);
|
|
3838
|
+
if (space === void 0) continue;
|
|
3839
|
+
const graph = space.graph();
|
|
3840
|
+
const refsByHash = listRefsByContractHash(space);
|
|
3841
|
+
spaces.push({
|
|
3842
|
+
space: spaceEntry.space,
|
|
3843
|
+
contracts: [...graph.nodes].map((hash) => ({
|
|
3844
|
+
hash,
|
|
3845
|
+
refs: [...refsByHash.get(hash) ?? []]
|
|
3846
|
+
})),
|
|
3847
|
+
migrations: [...graph.migrationByHash.values()].map((edge) => ({
|
|
3848
|
+
name: edge.dirName,
|
|
3849
|
+
hash: edge.migrationHash,
|
|
3850
|
+
fromContract: edge.from === "empty" ? null : edge.from,
|
|
3851
|
+
toContract: edge.to
|
|
3852
|
+
}))
|
|
3853
|
+
});
|
|
3854
|
+
}
|
|
3855
|
+
return spaces;
|
|
3856
|
+
}
|
|
3857
|
+
/**
|
|
3858
|
+
* Policy core of `migration new`: scaffolds a migration package with a `migration.ts` stub for manual authoring.
|
|
3859
|
+
*/
|
|
3860
|
+
async function executeMigrationNewCommand(options) {
|
|
3861
|
+
const config = options.config;
|
|
3862
|
+
const cwd = options.cwd;
|
|
3863
|
+
const { migrationsDir, appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(options.configPath, config, cwd);
|
|
3864
|
+
const stack = createControlStack(config);
|
|
3865
|
+
const familyInstance = config.family.create(stack);
|
|
3866
|
+
const controlAdapter = config.adapter.create(stack);
|
|
3867
|
+
const contractPathAbsolute = resolveContractPath(config);
|
|
3868
|
+
let contractJsonContent;
|
|
3869
|
+
try {
|
|
3870
|
+
contractJsonContent = await readFile(contractPathAbsolute, "utf-8");
|
|
3871
|
+
} catch (error) {
|
|
3872
|
+
if (error instanceof Error && error.code === "ENOENT") return notOk(errorRuntime("CLI.FILE_NOT_FOUND", `Contract file not found at ${contractPathAbsolute}`, {
|
|
3873
|
+
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
3874
|
+
fix: "Run `prisma-cli contract emit` first to generate the contract"
|
|
3875
|
+
}));
|
|
3876
|
+
throw error;
|
|
3877
|
+
}
|
|
3878
|
+
let toContract;
|
|
3879
|
+
try {
|
|
3880
|
+
const parsedContract = JSON.parse(contractJsonContent);
|
|
3881
|
+
toContract = familyInstance.deserializeContract(parsedContract);
|
|
3882
|
+
} catch (error) {
|
|
3883
|
+
return notOk(errorRuntime("MIGRATION.CONTRACT_DESERIALIZATION_FAILED", "Contract JSON is invalid", {
|
|
3884
|
+
why: `Failed to deserialize ${contractPathAbsolute}: ${error instanceof Error ? error.message : String(error)}`,
|
|
3885
|
+
fix: "Run `prisma-cli contract emit` to regenerate the contract",
|
|
3886
|
+
cause: error
|
|
3887
|
+
}));
|
|
3888
|
+
}
|
|
3889
|
+
const toStorageHash = toContract.storage?.storageHash;
|
|
3890
|
+
if (typeof toStorageHash !== "string") return notOk(errorRuntime("CONTRACT.VALIDATION_FAILED", "Contract is missing storageHash", {
|
|
3891
|
+
why: `Contract at ${contractPathAbsolute} has no storageHash`,
|
|
3892
|
+
fix: "Run `prisma-cli contract emit` to regenerate the contract"
|
|
3893
|
+
}));
|
|
3894
|
+
const aggregate = await loadContractSpaceAggregate({
|
|
3895
|
+
migrationsDir,
|
|
3896
|
+
deserializeContract: (json) => familyInstance.deserializeContract(json),
|
|
3897
|
+
appContract: toContract
|
|
3898
|
+
});
|
|
3899
|
+
const packageCorruptionFailure = refusePackageCorruptionOnAggregate(aggregate);
|
|
3900
|
+
if (packageCorruptionFailure) return notOk(packageCorruptionFailure);
|
|
3901
|
+
const packages = aggregate.app.packages;
|
|
3902
|
+
const graph = aggregate.app.graph();
|
|
3903
|
+
let fromHash = null;
|
|
3904
|
+
if (packages.length > 0) if (options.from) {
|
|
3905
|
+
const match = packages.find((p) => p.metadata.to.startsWith(options.from));
|
|
3906
|
+
if (!match) return notOk(errorRuntime("MIGRATION.HASH_NOT_IN_GRAPH", "Starting contract not found", {
|
|
3907
|
+
why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
|
|
3908
|
+
fix: "Check that the --from hash matches a known migration target hash."
|
|
3909
|
+
}));
|
|
3910
|
+
fromHash = match.metadata.to;
|
|
3911
|
+
} else {
|
|
3912
|
+
const latestMigration = findLatestMigration(graph);
|
|
3913
|
+
if (latestMigration) fromHash = latestMigration.to;
|
|
3914
|
+
}
|
|
3915
|
+
if (fromHash === toStorageHash && !options.from) return notOk(errorRuntime("MIGRATION.NO_CHANGES", "No changes detected", {
|
|
3916
|
+
why: "The from and to contract hashes are identical — there is nothing to migrate.",
|
|
3917
|
+
fix: "Change the contract and run `prisma-cli contract emit` before creating a new migration. To author a data-only migration on the current contract hash, pass `--from <hash>` explicitly."
|
|
3918
|
+
}));
|
|
3919
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
3920
|
+
const packageDir = join(appMigrationsDir, formatMigrationDirName(timestamp, options.name ?? "migration"));
|
|
3921
|
+
const baseMetadata = {
|
|
3922
|
+
from: fromHash,
|
|
3923
|
+
to: toStorageHash,
|
|
3924
|
+
providedInvariants: [],
|
|
3925
|
+
createdAt: timestamp.toISOString()
|
|
3926
|
+
};
|
|
3927
|
+
const metadata = {
|
|
3928
|
+
...baseMetadata,
|
|
3929
|
+
migrationHash: computeMigrationHash(baseMetadata, [])
|
|
3930
|
+
};
|
|
3931
|
+
const migrations = getTargetMigrations(config.target);
|
|
3932
|
+
if (!migrations) return notOk(errorTargetMigrationNotSupported({ why: `Target "${config.target.targetId}" does not support migrations` }));
|
|
3933
|
+
try {
|
|
3934
|
+
assertFrameworkComponentsCompatible(config.family.familyId, config.target.targetId, [
|
|
3935
|
+
config.target,
|
|
3936
|
+
config.adapter,
|
|
3937
|
+
...config.extensions ?? []
|
|
3938
|
+
]);
|
|
3939
|
+
const resolveSpecifier = createProjectSpecifierResolver(options.configPath);
|
|
3940
|
+
await writeMigrationPackage(packageDir, metadata, []);
|
|
3941
|
+
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
3942
|
+
const [contractJsonRaw, contractDts] = await Promise.all([readFile(destinationArtifacts.jsonPath, "utf-8"), readFile(destinationArtifacts.dtsPath, "utf-8")]);
|
|
3943
|
+
await writeContractSnapshot(migrationsDir, toStorageHash, {
|
|
3944
|
+
contractJson: JSON.parse(contractJsonRaw),
|
|
3945
|
+
contractDts
|
|
3946
|
+
});
|
|
3947
|
+
await writeMigrationTs(packageDir, migrations.createPlanner(controlAdapter).emptyMigration({
|
|
3948
|
+
packageDir,
|
|
3949
|
+
contractJsonPath: join(contractSnapshotDir(migrationsDir, toStorageHash), "contract.json"),
|
|
3950
|
+
fromHash,
|
|
3951
|
+
toHash: toStorageHash,
|
|
3952
|
+
snapshotsImportPath: snapshotsImportPathFrom(packageDir, migrationsDir)
|
|
3953
|
+
}, APP_SPACE_ID).renderTypeScript(resolveSpecifier));
|
|
3954
|
+
return ok({
|
|
3955
|
+
ok: true,
|
|
3956
|
+
dir: relative(cwd, packageDir),
|
|
3957
|
+
from: fromHash,
|
|
3958
|
+
to: toStorageHash,
|
|
3959
|
+
summary: `Scaffolded migration at ${relative(cwd, packageDir)}`
|
|
3960
|
+
});
|
|
3961
|
+
} catch (error) {
|
|
3962
|
+
if (CliStructuredError$1.is(error)) return notOk(error);
|
|
3963
|
+
return notOk(errorUnexpected(error instanceof Error ? error.message : String(error), { why: `Failed to scaffold migration: ${error instanceof Error ? error.message : String(error)}` }));
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Phase-1 of the two-phase `migration plan` pipeline (sub-spec § 4).
|
|
3968
|
+
*
|
|
3969
|
+
* For every extension that exposes a `contractSpace`:
|
|
3970
|
+
*
|
|
3971
|
+
* 1. Read the on-disk head ref (returns `null` on first emit).
|
|
3972
|
+
* 2. Write the head contract into the migrations-root snapshot store
|
|
3973
|
+
* (write-if-absent, keyed by hash) and unconditionally re-emit
|
|
3974
|
+
* `refs/head.json`, via {@link emitContractSpaceArtifacts}. The
|
|
3975
|
+
* framework owns `refs/head.json`; re-emit is the contract for that
|
|
3976
|
+
* file. The snapshot itself is only written once per distinct hash.
|
|
3977
|
+
* 3. Materialise any descriptor-shipped migration packages not yet on
|
|
3978
|
+
* disk via {@link materialiseExtensionMigrationPackageIfMissing}.
|
|
3979
|
+
* Existing packages are left untouched (by-existence skip).
|
|
3980
|
+
*
|
|
3981
|
+
* The return value lets the caller render a per-space status line and
|
|
3982
|
+
* lets the phase-2 aggregate loader run on a now-consistent disk state
|
|
3983
|
+
* (every loaded extension is guaranteed to have its head ref pinned
|
|
3984
|
+
* to the descriptor's hash and to ship every package the descriptor
|
|
3985
|
+
* declares).
|
|
3986
|
+
*
|
|
3987
|
+
* Output ordering is deterministic and alphabetical by spaceId (via
|
|
3988
|
+
* {@link planAllSpaces}, which also detects duplicate spaceIds). This
|
|
3989
|
+
* matches the canonical sort order used by every other aggregate
|
|
3990
|
+
* surface (`migrate`, `migration status`, the runner).
|
|
3991
|
+
*/
|
|
3992
|
+
async function runContractSpaceSeedPhase(inputs) {
|
|
3993
|
+
const planned = planAllSpaces(inputs.extensions.filter((pack) => pack.contractSpace !== void 0).map((pack) => ({
|
|
3994
|
+
spaceId: pack.id,
|
|
3995
|
+
priorContract: null,
|
|
3996
|
+
newContract: pack.contractSpace.contractJson,
|
|
3997
|
+
__pack: pack.contractSpace
|
|
3998
|
+
})), (input) => input.__pack.migrations);
|
|
3999
|
+
const descriptorBySpace = /* @__PURE__ */ new Map();
|
|
4000
|
+
for (const pack of inputs.extensions) if (pack.contractSpace !== void 0) descriptorBySpace.set(pack.id, pack.contractSpace);
|
|
4001
|
+
const seeded = [];
|
|
4002
|
+
for (const space of planned) {
|
|
4003
|
+
const descriptor = descriptorBySpace.get(space.spaceId);
|
|
4004
|
+
if (descriptor === void 0) continue;
|
|
4005
|
+
const priorHash = (await readContractSpaceHeadRef(inputs.migrationsDir, space.spaceId))?.hash ?? null;
|
|
4006
|
+
await emitContractSpaceArtifacts(inputs.migrationsDir, space.spaceId, {
|
|
4007
|
+
contract: descriptor.contractJson,
|
|
4008
|
+
contractDts: buildPlaceholderContractDts(space.spaceId),
|
|
4009
|
+
headRef: {
|
|
4010
|
+
hash: descriptor.headRef.hash,
|
|
4011
|
+
invariants: descriptor.headRef.invariants
|
|
4012
|
+
}
|
|
4013
|
+
});
|
|
4014
|
+
const spaceDir = spaceMigrationDirectory(inputs.migrationsDir, space.spaceId);
|
|
4015
|
+
const newMigrationDirs = [];
|
|
4016
|
+
for (const pkg of space.migrationPackages) {
|
|
4017
|
+
const { written } = await materialiseExtensionMigrationPackageIfMissing(spaceDir, pkg);
|
|
4018
|
+
if (written) newMigrationDirs.push(pkg.dirName);
|
|
4019
|
+
}
|
|
4020
|
+
const action = priorHash !== descriptor.headRef.hash || newMigrationDirs.length > 0 ? "updated" : "unchanged";
|
|
4021
|
+
seeded.push({
|
|
4022
|
+
spaceId: space.spaceId,
|
|
4023
|
+
action,
|
|
4024
|
+
priorHash,
|
|
4025
|
+
newHash: descriptor.headRef.hash,
|
|
4026
|
+
newMigrationDirs
|
|
4027
|
+
});
|
|
4028
|
+
}
|
|
4029
|
+
return { seeded };
|
|
4030
|
+
}
|
|
4031
|
+
/**
|
|
4032
|
+
* Placeholder `.d.ts` content for an extension space's on-disk mirror.
|
|
4033
|
+
*
|
|
4034
|
+
* Rendering a fully-typed `.d.ts` for an extension contract requires
|
|
4035
|
+
* the SQL-family renderer with the codec / typemap registry threaded
|
|
4036
|
+
* through; until that integration ships, the on-disk `.d.ts` is a
|
|
4037
|
+
* stub `export {};` module that documents how consumers should
|
|
4038
|
+
* validate the sibling `contract.json`. The stub typechecks on its
|
|
4039
|
+
* own and does not need any TypeScript suppressions.
|
|
4040
|
+
*/
|
|
4041
|
+
function buildPlaceholderContractDts(spaceId) {
|
|
4042
|
+
return [
|
|
4043
|
+
"/**",
|
|
4044
|
+
` * Placeholder \`.d.ts\` for extension space "${spaceId}".`,
|
|
4045
|
+
" *",
|
|
4046
|
+
" * The framework re-emits this file on every `migration plan` run",
|
|
4047
|
+
" * alongside `contract.json` and `refs/head.json`. A typed `.d.ts`",
|
|
4048
|
+
" * rendering pass for extension contracts is tracked separately;",
|
|
4049
|
+
" * until that ships, consumers should import `contract.json`",
|
|
4050
|
+
" * and pass it through the target descriptor’s `contractSerializer`.",
|
|
4051
|
+
" */",
|
|
4052
|
+
"export {};",
|
|
4053
|
+
""
|
|
4054
|
+
].join("\n");
|
|
4055
|
+
}
|
|
4056
|
+
const FULL_HASH_PATTERN = /^([0-9a-f]{64}|empty)$/;
|
|
4057
|
+
function looksLikeFullHash(input) {
|
|
4058
|
+
return FULL_HASH_PATTERN.test(input);
|
|
4059
|
+
}
|
|
4060
|
+
function graphIsEmpty(space) {
|
|
4061
|
+
return space.packages.length === 0;
|
|
4062
|
+
}
|
|
4063
|
+
function getReachableRefs(refs, graph) {
|
|
4064
|
+
return Object.entries(refs).flatMap(([name, entry]) => entry && isGraphNode(entry.hash, graph) ? [{
|
|
4065
|
+
name,
|
|
4066
|
+
hash: entry.hash
|
|
4067
|
+
}] : []).sort((a, b) => a.name.localeCompare(b.name));
|
|
4068
|
+
}
|
|
4069
|
+
function assertFromIsGraphNode(fromHash, graph, refs, graphTipHash) {
|
|
4070
|
+
try {
|
|
4071
|
+
assertHashIsGraphNode(fromHash, graph);
|
|
4072
|
+
} catch (error) {
|
|
4073
|
+
if (MigrationToolsError.is(error) && error.code === "MIGRATION.HASH_NOT_IN_GRAPH") throw errorPlanForgotTheFlag(fromHash, getReachableRefs(refs, graph), graphTipHash, { cause: error });
|
|
4074
|
+
throw error;
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
async function resolveContractRef(parsed, space, options) {
|
|
4078
|
+
const { hash, provenance } = parsed;
|
|
4079
|
+
const refName = provenance.kind === "ref" ? provenance.refName : void 0;
|
|
4080
|
+
try {
|
|
4081
|
+
const at = await space.contractAt(hash, refName !== void 0 ? { refName } : void 0);
|
|
4082
|
+
if (at.provenance === "ref") return ok({
|
|
4083
|
+
kind: "ref",
|
|
4084
|
+
hash: at.hash,
|
|
4085
|
+
contract: at.contract,
|
|
4086
|
+
contractJson: at.contractJson,
|
|
4087
|
+
contractDts: at.contractDts
|
|
4088
|
+
});
|
|
4089
|
+
return ok({
|
|
4090
|
+
kind: "graph-node",
|
|
4091
|
+
hash: at.hash,
|
|
4092
|
+
contract: at.contract,
|
|
4093
|
+
contractJson: at.contractJson,
|
|
4094
|
+
contractDts: at.contractDts
|
|
4095
|
+
});
|
|
4096
|
+
} catch (error) {
|
|
4097
|
+
return mapContractAtError(error, options?.artifactRole !== void 0 ? { artifactRole: options.artifactRole } : void 0);
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
async function resolveFromPolicy(parsed, input, refs, explicitFromLabel) {
|
|
4101
|
+
const resolution = await resolveContractRef(parsed, input.space, {
|
|
4102
|
+
...explicitFromLabel !== void 0 ? { explicitLabel: explicitFromLabel } : {},
|
|
4103
|
+
artifactRole: "from"
|
|
4104
|
+
});
|
|
4105
|
+
if (!resolution.ok) return resolution;
|
|
4106
|
+
if (resolution.value.kind === "graph-node") return ok({
|
|
4107
|
+
kind: "graph-node",
|
|
4108
|
+
fromHash: resolution.value.hash,
|
|
4109
|
+
fromContract: resolution.value.contract
|
|
4110
|
+
});
|
|
4111
|
+
const { hash, contract, contractJson, contractDts } = resolution.value;
|
|
4112
|
+
if (graphIsEmpty(input.space)) return ok({
|
|
4113
|
+
kind: "auto-baseline",
|
|
4114
|
+
fromHash: hash,
|
|
4115
|
+
fromContract: contract,
|
|
4116
|
+
contractDts,
|
|
4117
|
+
contractJson
|
|
4118
|
+
});
|
|
4119
|
+
const graph = input.space.graph();
|
|
4120
|
+
const graphTip = findLatestMigration(graph)?.to ?? null;
|
|
4121
|
+
try {
|
|
4122
|
+
assertFromIsGraphNode(hash, graph, refs, graphTip);
|
|
4123
|
+
} catch (error) {
|
|
4124
|
+
if (CliStructuredError$1.is(error)) return notOk(error);
|
|
4125
|
+
throw error;
|
|
4126
|
+
}
|
|
4127
|
+
return ok({
|
|
4128
|
+
kind: "ref",
|
|
4129
|
+
fromHash: hash,
|
|
4130
|
+
fromContract: contract,
|
|
4131
|
+
contractDts,
|
|
4132
|
+
contractJson
|
|
4133
|
+
});
|
|
4134
|
+
}
|
|
4135
|
+
async function resolveFromForPlan(input) {
|
|
4136
|
+
const { optionsFrom, space } = input;
|
|
4137
|
+
const graph = space.graph();
|
|
4138
|
+
const refs = space.refs;
|
|
4139
|
+
if (optionsFrom === void 0) {
|
|
4140
|
+
const dbRef = refs["db"];
|
|
4141
|
+
if (!dbRef) return ok({
|
|
4142
|
+
kind: "greenfield",
|
|
4143
|
+
fromHash: null,
|
|
4144
|
+
fromContract: null
|
|
4145
|
+
});
|
|
4146
|
+
return resolveFromPolicy({
|
|
4147
|
+
hash: dbRef.hash,
|
|
4148
|
+
provenance: {
|
|
4149
|
+
kind: "ref",
|
|
4150
|
+
refName: "db"
|
|
4151
|
+
}
|
|
4152
|
+
}, input, refs);
|
|
4153
|
+
}
|
|
4154
|
+
const refResult = parseContractRef(optionsFrom, {
|
|
4155
|
+
graph,
|
|
4156
|
+
refs
|
|
4157
|
+
});
|
|
4158
|
+
if (!refResult.ok) {
|
|
4159
|
+
if (looksLikeFullHash(optionsFrom)) {
|
|
4160
|
+
const empty = graphIsEmpty(space);
|
|
4161
|
+
const graphTip = findLatestMigration(graph)?.to ?? null;
|
|
4162
|
+
if (empty) return notOk(errorSnapshotMissing(optionsFrom, { viaRef: false }));
|
|
4163
|
+
return notOk(errorPlanForgotTheFlag(optionsFrom, getReachableRefs(refs, graph), graphTip));
|
|
4164
|
+
}
|
|
4165
|
+
return notOk(mapRefResolutionError(refResult.failure));
|
|
4166
|
+
}
|
|
4167
|
+
return resolveFromPolicy(refResult.value, input, refs, optionsFrom);
|
|
4168
|
+
}
|
|
4169
|
+
async function resolveToForPlan(optionsTo, input) {
|
|
4170
|
+
const { space } = input;
|
|
4171
|
+
const graph = space.graph();
|
|
4172
|
+
const refs = space.refs;
|
|
4173
|
+
const refResult = parseContractRef(optionsTo, {
|
|
4174
|
+
graph,
|
|
4175
|
+
refs
|
|
4176
|
+
});
|
|
4177
|
+
if (!refResult.ok) return notOk(mapRefResolutionError(refResult.failure));
|
|
4178
|
+
const resolution = await resolveContractRef(refResult.value, space, {
|
|
4179
|
+
explicitLabel: optionsTo,
|
|
4180
|
+
artifactRole: "to"
|
|
4181
|
+
});
|
|
4182
|
+
if (!resolution.ok) return resolution;
|
|
4183
|
+
const { hash, contract, contractJson, contractDts } = resolution.value;
|
|
4184
|
+
return ok({
|
|
4185
|
+
hash,
|
|
4186
|
+
contract,
|
|
4187
|
+
contractJson,
|
|
4188
|
+
contractDts
|
|
4189
|
+
});
|
|
4190
|
+
}
|
|
4191
|
+
/**
|
|
4192
|
+
* Policy core of `migration plan`: resolves from/to contracts, runs the planner legs, and writes the planned migration packages.
|
|
4193
|
+
*/
|
|
4194
|
+
function isEnoent(error) {
|
|
4195
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
4196
|
+
}
|
|
4197
|
+
async function runPlannerLeg(planner, migrations, frameworkComponents, contract, fromContract, spaceId, ownership, snapshotsImportPath, resolveImportSpecifier) {
|
|
4198
|
+
const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
|
|
4199
|
+
const plannerResult = planner.plan({
|
|
4200
|
+
contract,
|
|
4201
|
+
schema: fromSchema,
|
|
4202
|
+
policy: { allowedOperationClasses: [
|
|
4203
|
+
"additive",
|
|
4204
|
+
"widening",
|
|
4205
|
+
"destructive",
|
|
4206
|
+
"data"
|
|
4207
|
+
] },
|
|
4208
|
+
fromContract,
|
|
4209
|
+
frameworkComponents,
|
|
4210
|
+
spaceId,
|
|
4211
|
+
ownership,
|
|
4212
|
+
snapshotsImportPath
|
|
4213
|
+
});
|
|
4214
|
+
if (plannerResult.kind === "failure") return notOk(errorMigrationPlanningFailed({ conflicts: castAs(plannerResult.conflicts) }));
|
|
4215
|
+
let plannedOps = [];
|
|
4216
|
+
let hasPlaceholders = false;
|
|
4217
|
+
try {
|
|
4218
|
+
plannedOps = await Promise.all(plannerResult.plan.operations);
|
|
4219
|
+
if (plannedOps.length === 0) return notOk(errorMigrationPlanningFailed({ conflicts: [{
|
|
4220
|
+
kind: "unsupportedChange",
|
|
4221
|
+
summary: "Contract changed but planner produced no operations. This indicates unsupported or ignored changes."
|
|
4222
|
+
}] }));
|
|
4223
|
+
} catch (e) {
|
|
4224
|
+
if (CliStructuredError$1.is(e) && e.code === "MIGRATION.UNFILLED_PLACEHOLDER") hasPlaceholders = true;
|
|
4225
|
+
else throw e;
|
|
4226
|
+
}
|
|
4227
|
+
return ok({
|
|
4228
|
+
plannedOps,
|
|
4229
|
+
migrationTsContent: plannerResult.plan.renderTypeScript(resolveImportSpecifier),
|
|
4230
|
+
hasPlaceholders
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
async function writePlannedMigrationPackage(packageDir, fromHash, toHash, createdAt, leg) {
|
|
4234
|
+
const opsForWrite = leg.hasPlaceholders ? [] : leg.plannedOps;
|
|
4235
|
+
const metadataWithInvariants = {
|
|
4236
|
+
from: fromHash,
|
|
4237
|
+
to: toHash,
|
|
4238
|
+
providedInvariants: deriveProvidedInvariants(opsForWrite),
|
|
4239
|
+
createdAt: createdAt.toISOString()
|
|
4240
|
+
};
|
|
4241
|
+
await writeMigrationPackage(packageDir, {
|
|
4242
|
+
...metadataWithInvariants,
|
|
4243
|
+
migrationHash: computeMigrationHash(metadataWithInvariants, opsForWrite)
|
|
4244
|
+
}, opsForWrite);
|
|
4245
|
+
await writeMigrationTs(packageDir, leg.migrationTsContent);
|
|
4246
|
+
}
|
|
4247
|
+
async function executeMigrationPlanCommand(options, startTime, callbacks) {
|
|
4248
|
+
try {
|
|
4249
|
+
return await executeMigrationPlanCommandInner(options, startTime, callbacks);
|
|
4250
|
+
} catch (error) {
|
|
4251
|
+
if (CliStructuredError$1.is(error)) return notOk(error);
|
|
4252
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4253
|
+
return notOk(errorUnexpected(message, { why: `Unexpected error during migration plan: ${message}` }));
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
async function executeMigrationPlanCommandInner(options, startTime, callbacks) {
|
|
4257
|
+
const config = options.config;
|
|
4258
|
+
const cwd = options.cwd;
|
|
4259
|
+
const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(options.configPath, config, cwd);
|
|
4260
|
+
const contractPathAbsolute = resolveContractPath(config);
|
|
4261
|
+
const contractPath = relative(cwd, contractPathAbsolute);
|
|
4262
|
+
callbacks?.onContextResolved?.({
|
|
4263
|
+
configPath,
|
|
4264
|
+
contractPath,
|
|
4265
|
+
appMigrationsRelative
|
|
4266
|
+
});
|
|
4267
|
+
let contractJsonContent;
|
|
4268
|
+
try {
|
|
4269
|
+
contractJsonContent = await readFile(contractPathAbsolute, "utf-8");
|
|
4270
|
+
} catch (error) {
|
|
4271
|
+
if (isEnoent(error)) return notOk(errorFileNotFound(contractPathAbsolute, {
|
|
4272
|
+
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
4273
|
+
fix: `Run \`prisma-cli contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`
|
|
4274
|
+
}));
|
|
4275
|
+
return notOk(errorUnexpected(error instanceof Error ? error.message : String(error), { why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}` }));
|
|
4276
|
+
}
|
|
4277
|
+
const stack = createControlStack(config);
|
|
4278
|
+
const familyInstance = config.family.create(stack);
|
|
4279
|
+
const controlAdapter = config.adapter.create(stack);
|
|
4280
|
+
let toContract;
|
|
4281
|
+
try {
|
|
4282
|
+
toContract = familyInstance.deserializeContract(castAs(JSON.parse(contractJsonContent)));
|
|
4283
|
+
} catch (error) {
|
|
4284
|
+
return notOk(errorContractValidationFailed(`Contract at ${contractPathAbsolute} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`, { where: { path: contractPathAbsolute } }));
|
|
4285
|
+
}
|
|
4286
|
+
const rawStorageHash = toContract.storage?.storageHash;
|
|
4287
|
+
if (typeof rawStorageHash !== "string") return notOk(errorContractValidationFailed("Contract is missing storageHash", { where: { path: contractPathAbsolute } }));
|
|
4288
|
+
let toStorageHash = rawStorageHash;
|
|
4289
|
+
let toArtifacts = null;
|
|
4290
|
+
let fromContract = null;
|
|
4291
|
+
let fromHash = null;
|
|
4292
|
+
let snapshotStartContract = null;
|
|
4293
|
+
let isAutoBaseline = false;
|
|
4294
|
+
const tolerantAggregateResult = await loadContractSpaceAggregateForCli({
|
|
4295
|
+
targetId: config.target.targetId,
|
|
4296
|
+
migrationsDir,
|
|
4297
|
+
appContract: toContract,
|
|
4298
|
+
extensions: config.extensions ?? [],
|
|
4299
|
+
deserializeContract: (json) => familyInstance.deserializeContract(json)
|
|
4300
|
+
});
|
|
4301
|
+
if (!tolerantAggregateResult.ok) return notOk(tolerantAggregateResult.failure);
|
|
4302
|
+
const resolutionSpace = tolerantAggregateResult.value.app;
|
|
4303
|
+
const resolutionResult = await resolveFromForPlan({
|
|
4304
|
+
optionsFrom: options.from,
|
|
4305
|
+
space: resolutionSpace
|
|
4306
|
+
});
|
|
4307
|
+
if (!resolutionResult.ok) return notOk(resolutionResult.failure);
|
|
4308
|
+
switch (resolutionResult.value.kind) {
|
|
4309
|
+
case "greenfield": break;
|
|
4310
|
+
case "graph-node":
|
|
4311
|
+
fromHash = resolutionResult.value.fromHash;
|
|
4312
|
+
fromContract = resolutionResult.value.fromContract;
|
|
4313
|
+
break;
|
|
4314
|
+
case "ref":
|
|
4315
|
+
fromHash = resolutionResult.value.fromHash;
|
|
4316
|
+
fromContract = resolutionResult.value.fromContract;
|
|
4317
|
+
snapshotStartContract = {
|
|
4318
|
+
fromHash: resolutionResult.value.fromHash,
|
|
4319
|
+
contractJson: resolutionResult.value.contractJson,
|
|
4320
|
+
contractDts: resolutionResult.value.contractDts
|
|
4321
|
+
};
|
|
4322
|
+
break;
|
|
4323
|
+
case "auto-baseline":
|
|
4324
|
+
fromHash = resolutionResult.value.fromHash;
|
|
4325
|
+
fromContract = resolutionResult.value.fromContract;
|
|
4326
|
+
snapshotStartContract = {
|
|
4327
|
+
fromHash: resolutionResult.value.fromHash,
|
|
4328
|
+
contractJson: resolutionResult.value.contractJson,
|
|
4329
|
+
contractDts: resolutionResult.value.contractDts
|
|
4330
|
+
};
|
|
4331
|
+
isAutoBaseline = true;
|
|
4332
|
+
break;
|
|
4333
|
+
}
|
|
4334
|
+
if (options.to !== void 0) {
|
|
4335
|
+
const toResolution = await resolveToForPlan(options.to, { space: resolutionSpace });
|
|
4336
|
+
if (!toResolution.ok) return notOk(toResolution.failure);
|
|
4337
|
+
toContract = toResolution.value.contract;
|
|
4338
|
+
toStorageHash = toResolution.value.hash;
|
|
4339
|
+
toArtifacts = {
|
|
4340
|
+
contractJson: toResolution.value.contractJson,
|
|
4341
|
+
contractDts: toResolution.value.contractDts
|
|
4342
|
+
};
|
|
4343
|
+
}
|
|
4344
|
+
const resolveImportSpecifier = createProjectSpecifierResolver(options.configPath);
|
|
4345
|
+
const seedResult = await runContractSpaceSeedPhase({
|
|
4346
|
+
migrationsDir,
|
|
4347
|
+
extensions: toExtensionInputs(config.extensions ?? [])
|
|
4348
|
+
});
|
|
4349
|
+
for (const record of seedResult.seeded) callbacks?.onSeeded?.(record);
|
|
4350
|
+
const emittedExtensionDirs = seedResult.seeded.flatMap((r) => r.newMigrationDirs.map((dirName) => ({
|
|
4351
|
+
spaceId: r.spaceId,
|
|
4352
|
+
dirName
|
|
4353
|
+
})));
|
|
4354
|
+
if (fromHash === toStorageHash && !isAutoBaseline) return ok({
|
|
4355
|
+
ok: true,
|
|
4356
|
+
noOp: true,
|
|
4357
|
+
from: fromHash,
|
|
4358
|
+
to: toStorageHash,
|
|
4359
|
+
operations: [],
|
|
4360
|
+
emittedExtensionDirs,
|
|
4361
|
+
summary: "No changes detected between contracts",
|
|
4362
|
+
timings: { total: Date.now() - startTime }
|
|
4363
|
+
});
|
|
4364
|
+
const migrations = getTargetMigrations(config.target);
|
|
4365
|
+
if (!migrations) return notOk(errorTargetMigrationNotSupported({ why: `Target "${config.target.id}" does not support migrations` }));
|
|
4366
|
+
const aggregateResult = await buildContractSpaceAggregate({
|
|
4367
|
+
targetId: config.target.targetId,
|
|
4368
|
+
migrationsDir,
|
|
4369
|
+
appContract: toContract,
|
|
4370
|
+
extensions: config.extensions ?? [],
|
|
4371
|
+
deserializeContract: (json) => familyInstance.deserializeContract(json)
|
|
4372
|
+
});
|
|
4373
|
+
if (!aggregateResult.ok) return notOk(aggregateResult.failure);
|
|
4374
|
+
const aggregate = aggregateResult.value;
|
|
4375
|
+
const frameworkComponents = assertFrameworkComponentsCompatible(config.family.familyId, config.target.targetId, [
|
|
4376
|
+
config.target,
|
|
4377
|
+
config.adapter,
|
|
4378
|
+
...config.extensions ?? []
|
|
4379
|
+
]);
|
|
4380
|
+
async function writeDestinationSnapshot(destHash) {
|
|
4381
|
+
if (toArtifacts !== null) {
|
|
4382
|
+
await writeContractSnapshot(migrationsDir, destHash, {
|
|
4383
|
+
contractJson: toArtifacts.contractJson,
|
|
4384
|
+
contractDts: toArtifacts.contractDts
|
|
4385
|
+
});
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
4389
|
+
const [contractJsonRaw, contractDts] = await Promise.all([readFile(destinationArtifacts.jsonPath, "utf-8"), readFile(destinationArtifacts.dtsPath, "utf-8")]);
|
|
4390
|
+
await writeContractSnapshot(migrationsDir, destHash, {
|
|
4391
|
+
contractJson: castAs(JSON.parse(contractJsonRaw)),
|
|
4392
|
+
contractDts
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
try {
|
|
4396
|
+
const planner = migrations.createPlanner(controlAdapter);
|
|
4397
|
+
if (isAutoBaseline && fromHash !== null && fromContract !== null && snapshotStartContract !== null) {
|
|
4398
|
+
const baselineTimestamp = /* @__PURE__ */ new Date();
|
|
4399
|
+
const deltaTimestamp = new Date(baselineTimestamp.getTime() + 6e4);
|
|
4400
|
+
const baselineDirName = formatMigrationDirName(baselineTimestamp, "baseline");
|
|
4401
|
+
const deltaDirName = formatMigrationDirName(deltaTimestamp, options.name ?? "migration");
|
|
4402
|
+
const baselinePackageDir = join(appMigrationsDir, baselineDirName);
|
|
4403
|
+
const deltaPackageDir = join(appMigrationsDir, deltaDirName);
|
|
4404
|
+
const baselineLeg = await runPlannerLeg(planner, migrations, frameworkComponents, fromContract, null, aggregate.app.spaceId, aggregate, snapshotsImportPathFrom(baselinePackageDir, migrationsDir), resolveImportSpecifier);
|
|
4405
|
+
if (!baselineLeg.ok) return notOk(baselineLeg.failure);
|
|
4406
|
+
await writePlannedMigrationPackage(baselinePackageDir, null, fromHash, baselineTimestamp, baselineLeg.value);
|
|
4407
|
+
await writeContractSnapshot(migrationsDir, fromHash, {
|
|
4408
|
+
contractJson: snapshotStartContract.contractJson,
|
|
4409
|
+
contractDts: snapshotStartContract.contractDts
|
|
4410
|
+
});
|
|
4411
|
+
if (fromHash === toStorageHash) {
|
|
4412
|
+
const baselineOps = baselineLeg.value.hasPlaceholders ? [] : baselineLeg.value.plannedOps;
|
|
4413
|
+
if (baselineLeg.value.hasPlaceholders) {
|
|
4414
|
+
const baselineDir = relative(cwd, baselinePackageDir);
|
|
4415
|
+
return ok({
|
|
4416
|
+
ok: true,
|
|
4417
|
+
noOp: false,
|
|
4418
|
+
from: fromHash,
|
|
4419
|
+
to: toStorageHash,
|
|
4420
|
+
dir: baselineDir,
|
|
4421
|
+
baselineDir,
|
|
4422
|
+
operations: [],
|
|
4423
|
+
emittedExtensionDirs,
|
|
4424
|
+
pendingPlaceholders: true,
|
|
4425
|
+
summary: "Planned baseline with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit",
|
|
4426
|
+
timings: { total: Date.now() - startTime }
|
|
4427
|
+
});
|
|
4428
|
+
}
|
|
4429
|
+
const preview = hasOperationPreview(familyInstance) ? familyInstance.toOperationPreview(baselineOps) : void 0;
|
|
4430
|
+
return ok({
|
|
4431
|
+
ok: true,
|
|
4432
|
+
noOp: false,
|
|
4433
|
+
from: fromHash,
|
|
4434
|
+
to: toStorageHash,
|
|
4435
|
+
baselineDir: relative(cwd, baselinePackageDir),
|
|
4436
|
+
operations: baselineOps.map((op) => ({
|
|
4437
|
+
id: op.id,
|
|
4438
|
+
label: op.label,
|
|
4439
|
+
operationClass: op.operationClass
|
|
4440
|
+
})),
|
|
4441
|
+
emittedExtensionDirs,
|
|
4442
|
+
...preview !== void 0 ? { preview } : {},
|
|
4443
|
+
summary: buildAutoBaselinePlanSummary(0, emittedExtensionDirs.length),
|
|
4444
|
+
timings: { total: Date.now() - startTime }
|
|
4445
|
+
});
|
|
4446
|
+
}
|
|
4447
|
+
const deltaLeg = await runPlannerLeg(planner, migrations, frameworkComponents, aggregate.app.contract(), fromContract, aggregate.app.spaceId, aggregate, snapshotsImportPathFrom(deltaPackageDir, migrationsDir), resolveImportSpecifier);
|
|
4448
|
+
if (!deltaLeg.ok) return notOk(deltaLeg.failure);
|
|
4449
|
+
await writePlannedMigrationPackage(deltaPackageDir, fromHash, toStorageHash, deltaTimestamp, deltaLeg.value);
|
|
4450
|
+
await writeDestinationSnapshot(toStorageHash);
|
|
4451
|
+
await writeContractSnapshot(migrationsDir, fromHash, {
|
|
4452
|
+
contractJson: snapshotStartContract.contractJson,
|
|
4453
|
+
contractDts: snapshotStartContract.contractDts
|
|
4454
|
+
});
|
|
4455
|
+
const deltaOps = deltaLeg.value.hasPlaceholders ? [] : deltaLeg.value.plannedOps;
|
|
4456
|
+
if (deltaLeg.value.hasPlaceholders) return ok({
|
|
4457
|
+
ok: true,
|
|
4458
|
+
noOp: false,
|
|
4459
|
+
from: fromHash,
|
|
4460
|
+
to: toStorageHash,
|
|
4461
|
+
dir: relative(cwd, deltaPackageDir),
|
|
4462
|
+
baselineDir: relative(cwd, baselinePackageDir),
|
|
4463
|
+
operations: [],
|
|
4464
|
+
emittedExtensionDirs,
|
|
4465
|
+
pendingPlaceholders: true,
|
|
4466
|
+
summary: "Planned baseline + migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit",
|
|
4467
|
+
timings: { total: Date.now() - startTime }
|
|
4468
|
+
});
|
|
4469
|
+
const preview = hasOperationPreview(familyInstance) ? familyInstance.toOperationPreview(deltaOps) : void 0;
|
|
4470
|
+
return ok({
|
|
4471
|
+
ok: true,
|
|
4472
|
+
noOp: false,
|
|
4473
|
+
from: fromHash,
|
|
4474
|
+
to: toStorageHash,
|
|
4475
|
+
dir: relative(cwd, deltaPackageDir),
|
|
4476
|
+
baselineDir: relative(cwd, baselinePackageDir),
|
|
4477
|
+
operations: deltaOps.map((op) => ({
|
|
4478
|
+
id: op.id,
|
|
4479
|
+
label: op.label,
|
|
4480
|
+
operationClass: op.operationClass
|
|
4481
|
+
})),
|
|
4482
|
+
emittedExtensionDirs,
|
|
4483
|
+
...preview !== void 0 ? { preview } : {},
|
|
4484
|
+
summary: buildAutoBaselinePlanSummary(deltaOps.length, emittedExtensionDirs.length),
|
|
4485
|
+
timings: { total: Date.now() - startTime }
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4488
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
4489
|
+
const packageDir = join(appMigrationsDir, formatMigrationDirName(timestamp, options.name ?? "migration"));
|
|
4490
|
+
const deltaLeg = await runPlannerLeg(planner, migrations, frameworkComponents, aggregate.app.contract(), fromContract, aggregate.app.spaceId, aggregate, snapshotsImportPathFrom(packageDir, migrationsDir), resolveImportSpecifier);
|
|
4491
|
+
if (!deltaLeg.ok) return notOk(deltaLeg.failure);
|
|
4492
|
+
await writePlannedMigrationPackage(packageDir, fromHash, toStorageHash, timestamp, deltaLeg.value);
|
|
4493
|
+
await writeDestinationSnapshot(toStorageHash);
|
|
4494
|
+
if (snapshotStartContract !== null) await writeContractSnapshot(migrationsDir, snapshotStartContract.fromHash, {
|
|
4495
|
+
contractJson: snapshotStartContract.contractJson,
|
|
4496
|
+
contractDts: snapshotStartContract.contractDts
|
|
4497
|
+
});
|
|
4498
|
+
if (deltaLeg.value.hasPlaceholders) return ok({
|
|
4499
|
+
ok: true,
|
|
4500
|
+
noOp: false,
|
|
4501
|
+
from: fromHash,
|
|
4502
|
+
to: toStorageHash,
|
|
4503
|
+
dir: relative(cwd, packageDir),
|
|
4504
|
+
operations: [],
|
|
4505
|
+
emittedExtensionDirs,
|
|
4506
|
+
pendingPlaceholders: true,
|
|
4507
|
+
summary: "Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit",
|
|
4508
|
+
timings: { total: Date.now() - startTime }
|
|
4509
|
+
});
|
|
4510
|
+
const plannedOps = deltaLeg.value.plannedOps;
|
|
4511
|
+
const preview = hasOperationPreview(familyInstance) ? familyInstance.toOperationPreview(plannedOps) : void 0;
|
|
4512
|
+
return ok({
|
|
4513
|
+
ok: true,
|
|
4514
|
+
noOp: false,
|
|
4515
|
+
from: fromHash,
|
|
4516
|
+
to: toStorageHash,
|
|
4517
|
+
dir: relative(cwd, packageDir),
|
|
4518
|
+
operations: plannedOps.map((op) => ({
|
|
4519
|
+
id: op.id,
|
|
4520
|
+
label: op.label,
|
|
4521
|
+
operationClass: op.operationClass
|
|
4522
|
+
})),
|
|
4523
|
+
emittedExtensionDirs,
|
|
4524
|
+
...preview !== void 0 ? { preview } : {},
|
|
4525
|
+
summary: buildPlanSummary(plannedOps.length, emittedExtensionDirs.length),
|
|
4526
|
+
timings: { total: Date.now() - startTime }
|
|
4527
|
+
});
|
|
4528
|
+
} catch (error) {
|
|
4529
|
+
if (CliStructuredError$1.is(error)) return notOk(error);
|
|
4530
|
+
if (MigrationToolsError.is(error)) return notOk(error);
|
|
4531
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4532
|
+
return notOk(errorUnexpected(message, { why: `Unexpected error during migration plan: ${message}` }));
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
/**
|
|
4536
|
+
* Compose the success-line summary so the cross-space side effect
|
|
4537
|
+
* (extension-space migration packages materialised on disk during
|
|
4538
|
+
* this `plan` run) is visible in the top line — not just in the
|
|
4539
|
+
* step log above it.
|
|
4540
|
+
*
|
|
4541
|
+
* Example outputs:
|
|
4542
|
+
* - `Planned 3 operation(s)` (app-space-only project)
|
|
4543
|
+
* - `Planned 3 operation(s); materialised 1 extension-space migration` (one extension)
|
|
4544
|
+
* - `Planned 3 operation(s); materialised 2 extension-space migrations` (two extensions)
|
|
4545
|
+
*
|
|
4546
|
+
* Locks AC3 at the summary-line level: a reader of the success line
|
|
4547
|
+
* can tell that something happened beyond the app space.
|
|
4548
|
+
*/
|
|
4549
|
+
function buildPlanSummary(plannedOpsCount, emittedExtensionDirsCount) {
|
|
4550
|
+
const base = `Planned ${plannedOpsCount} operation(s)`;
|
|
4551
|
+
if (emittedExtensionDirsCount === 0) return base;
|
|
4552
|
+
return `${base}; materialised ${emittedExtensionDirsCount} ${emittedExtensionDirsCount === 1 ? "extension-space migration" : "extension-space migrations"}`;
|
|
4553
|
+
}
|
|
4554
|
+
function buildAutoBaselinePlanSummary(deltaOpsCount, emittedExtensionDirsCount) {
|
|
4555
|
+
const base = `Planned baseline + ${deltaOpsCount} operation(s)`;
|
|
4556
|
+
if (emittedExtensionDirsCount === 0) return base;
|
|
4557
|
+
return `${base}; materialised ${emittedExtensionDirsCount} ${emittedExtensionDirsCount === 1 ? "extension-space migration" : "extension-space migrations"}`;
|
|
4558
|
+
}
|
|
4559
|
+
/** Origin hash for status path computation: the live/override marker, or the empty-contract sentinel. */
|
|
4560
|
+
function originHashForStatus(markerHash) {
|
|
4561
|
+
return markerHash ?? "empty";
|
|
4562
|
+
}
|
|
4563
|
+
function deriveStatusEdgeAnnotations(input) {
|
|
4564
|
+
const annotations = /* @__PURE__ */ new Map();
|
|
4565
|
+
if (input.showAppliedOverlay) {
|
|
4566
|
+
for (const edge of input.graph.migrationByHash.values()) if (input.appliedMigrationHashes.has(edge.migrationHash)) annotations.set(edge.migrationHash, { status: "applied" });
|
|
4567
|
+
}
|
|
4568
|
+
if (!input.graph.nodes.has(input.originHash)) return annotations;
|
|
4569
|
+
const pendingPath = findPath(input.graph, input.originHash, input.targetHash);
|
|
4570
|
+
if (!pendingPath) return annotations;
|
|
4571
|
+
for (const edge of pendingPath) {
|
|
4572
|
+
if (input.appliedMigrationHashes.has(edge.migrationHash)) continue;
|
|
4573
|
+
if (annotations.get(edge.migrationHash)?.status === "applied") continue;
|
|
4574
|
+
annotations.set(edge.migrationHash, { status: "pending" });
|
|
4575
|
+
}
|
|
4576
|
+
return annotations;
|
|
4577
|
+
}
|
|
4578
|
+
function appliedHashesFromLedger(ledgerEntries) {
|
|
4579
|
+
return new Set(ledgerEntries.map((entry) => entry.migrationHash));
|
|
4580
|
+
}
|
|
4581
|
+
function statusForMigrationHash(migrationHash, annotations) {
|
|
4582
|
+
return annotations.get(migrationHash)?.status ?? null;
|
|
4583
|
+
}
|
|
4584
|
+
/**
|
|
4585
|
+
* Reads the migrations refs index for commands, mapping MigrationToolsError into the CLI envelope.
|
|
4586
|
+
*/
|
|
4587
|
+
/** Reads migrations/<app>/refs, passing MigrationToolsError through; other errors rethrow. */
|
|
4588
|
+
async function readMigrationRefs(refsDir) {
|
|
4589
|
+
try {
|
|
4590
|
+
return ok(await readRefs(refsDir));
|
|
4591
|
+
} catch (error) {
|
|
4592
|
+
if (MigrationToolsError.is(error)) return notOk(error);
|
|
4593
|
+
throw error;
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
function mapError(error) {
|
|
4597
|
+
if (MigrationToolsError.is(error)) return error;
|
|
4598
|
+
return errorUnexpected(error instanceof Error ? error.message : String(error));
|
|
4599
|
+
}
|
|
4600
|
+
function cliErrorInvalidRefName(name) {
|
|
4601
|
+
return errorRuntime("MIGRATION.INVALID_REF_NAME", `Invalid ref name "${name}"`, {
|
|
4602
|
+
why: `Ref name "${name}" does not match the required format`,
|
|
4603
|
+
fix: "Ref names must be lowercase alphanumeric with hyphens or forward slashes, no `.` or `..` segments"
|
|
4604
|
+
});
|
|
4605
|
+
}
|
|
4606
|
+
async function executeRefSetCommand(name, contractInput, options) {
|
|
4607
|
+
if (!validateRefName(name)) return notOk(cliErrorInvalidRefName(name));
|
|
4608
|
+
const config = options.config;
|
|
4609
|
+
try {
|
|
4610
|
+
const { migrationsDir, refsDir } = resolveMigrationPaths(options.configPath, config, options.cwd);
|
|
4611
|
+
const loaded = await buildReadAggregate(config, { migrationsDir });
|
|
4612
|
+
if (!loaded.ok) return notOk(loaded.failure);
|
|
4613
|
+
const graph = loaded.value.aggregate.app.graph();
|
|
4614
|
+
const bundles = loaded.value.aggregate.app.packages;
|
|
4615
|
+
const refs = loaded.value.aggregate.app.refs;
|
|
4616
|
+
let resolvedHash;
|
|
4617
|
+
if (validateRefValue(contractInput)) resolvedHash = contractInput;
|
|
4618
|
+
else {
|
|
4619
|
+
const refResult = parseContractRef(contractInput, {
|
|
4620
|
+
graph,
|
|
4621
|
+
refs
|
|
4622
|
+
});
|
|
4623
|
+
if (!refResult.ok) return notOk(mapRefResolutionError(refResult.failure));
|
|
4624
|
+
resolvedHash = refResult.value.hash;
|
|
4625
|
+
}
|
|
4626
|
+
if (resolvedHash === "empty") return notOk(errorRefSetEmptySentinel(resolvedHash));
|
|
4627
|
+
if (!isGraphNode(resolvedHash, graph)) {
|
|
4628
|
+
const graphTip = findLatestMigration(graph)?.to ?? null;
|
|
4629
|
+
return notOk(errorRefSetHashNotInGraph(resolvedHash, [...graph.nodes].sort(), graphTip));
|
|
4630
|
+
}
|
|
4631
|
+
if (!bundles.find((bundle) => bundle.metadata.to === resolvedHash)) return notOk(errorRefSetBundleNotFound(resolvedHash));
|
|
4632
|
+
const contractJsonPath = join(contractSnapshotDir(migrationsDir, resolvedHash), "contract.json");
|
|
4633
|
+
try {
|
|
4634
|
+
await readContractSnapshotJson(migrationsDir, resolvedHash);
|
|
4635
|
+
} catch (readError) {
|
|
4636
|
+
if (MigrationToolsError.is(readError) && readError.code === "MIGRATION.CONTRACT_SNAPSHOT_MISSING") return notOk(errorFileNotFound(contractJsonPath, {
|
|
4637
|
+
why: `Migration bundle for hash ${resolvedHash} is missing its contract snapshot at ${contractJsonPath}`,
|
|
4638
|
+
fix: "Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot."
|
|
4639
|
+
}));
|
|
4640
|
+
throw readError;
|
|
4641
|
+
}
|
|
4642
|
+
await writeRef(refsDir, name, {
|
|
4643
|
+
hash: resolvedHash,
|
|
4644
|
+
invariants: []
|
|
4645
|
+
});
|
|
4646
|
+
return ok({
|
|
4647
|
+
ok: true,
|
|
4648
|
+
ref: name,
|
|
4649
|
+
hash: resolvedHash,
|
|
4650
|
+
invariants: []
|
|
4651
|
+
});
|
|
4652
|
+
} catch (error) {
|
|
4653
|
+
if (error instanceof CliStructuredError$1) return notOk(error);
|
|
4654
|
+
return notOk(mapError(error));
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
async function executeRefDeleteCommand(name, options) {
|
|
4658
|
+
try {
|
|
4659
|
+
const { refsDir } = resolveMigrationPaths(options.configPath, options.config, options.cwd);
|
|
4660
|
+
await deleteRef(refsDir, name);
|
|
4661
|
+
return ok({
|
|
4662
|
+
ok: true,
|
|
4663
|
+
ref: name,
|
|
4664
|
+
deleted: true
|
|
4665
|
+
});
|
|
4666
|
+
} catch (error) {
|
|
4667
|
+
if (error instanceof CliStructuredError$1) return notOk(error);
|
|
4668
|
+
return notOk(mapError(error));
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
async function executeRefListCommand(options) {
|
|
4672
|
+
try {
|
|
4673
|
+
const { refsDir } = resolveMigrationPaths(options.configPath, options.config, options.cwd);
|
|
4674
|
+
return ok({
|
|
4675
|
+
ok: true,
|
|
4676
|
+
refs: await readRefs(refsDir)
|
|
4677
|
+
});
|
|
4678
|
+
} catch (error) {
|
|
4679
|
+
if (error instanceof CliStructuredError$1) return notOk(error);
|
|
4680
|
+
return notOk(mapError(error));
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
//#endregion
|
|
4684
|
+
export { loadContractRawSafely as $, errorMigrationPlanningFailed as A, statusForMigrationHash as At, executeDbUpdate as B, errorDestructiveChanges as C, resolveRefAdvancementFields as Ct, errorLegendHumanOnly as D, runMigrationCheck as Dt, errorHashMismatch as E, runContractSpaceSeedPhase as Et, errorTargetMigrationNotSupported as F, executeRefAdvancement as G, executeMigrateShowPlan as H, errorTargetMismatch as I, executeRefSetCommand as J, executeRefDeleteCommand as K, errorUnexpected as L, errorPathUnreachable as M, toDeclaredExtensionsFromRaw as Mt, errorRunnerFailed as N, errorMarkerMissing as O, runMigrationList as Ot, errorRuntime as P, loadAggregateIntegrityViolations as Q, executeContractEmit as R, errorDatabaseConnectionRequired as S, resolveMigrationRef as St, errorFileNotFound as T, runCommandAction as Tt, executeMigrationNewCommand as U, executeDbVerify as V, executeMigrationPlanCommand as W, hasMigrationPath as X, findPackageByDirPath as Y, listRefsByContractHash as Z, enumerateCheckSpaces as _, requireLiveDatabase as _t, buildContractSpaceAggregate as a, maskConnectionUrl as at, errorContractArgConflict as b, resolveContractRefToSnapshot as bt, buildRefAdvancementFields as c, readContractEnvelope as ct, closeQuietly as d, refuseContractSpaceIntegrity as dt, loadContractSpaceAggregateForCli as et, computeRefAdvancementName as f, refuseDeclaredExtensionTargetMismatch as ft, enrichContract as g, refuseUnknownInvariants as gt, disposeEmitQueue as h, refusePackageCorruptionOnAggregate as ht, appliedHashesFromLedger as i, mapIntegrityViolations as it, errorNoMigrations as j, targetSupportsMigrations as jt, errorMigrationPackageNotFound as k, sanitizeErrorMessage as kt, checkSingleTarget as l, readContractIR as lt, deriveStatusEdgeAnnotations as m, refuseMissingInvariantPath as mt, advanceRefSafely as n, mapCaughtMigrationError as nt, buildMigrationSpaceGraphEntries as o, migrationSpaceListEntriesFromAggregate as ot, createControlClient as p, refuseMarkerOutsideGraph as pt, executeRefListCommand as q, appContractStandInFromIdentity as r, mapContractAtError as rt, buildReadAggregate as s, originHashForStatus as st, CliStructuredError$1 as t, looksLikePath as tt, chooseAction as u, readMigrationRefs as ut, errorConfigValidation$1 as v, resolveAppTargetPath as vt, errorDriverRequired as w, resolveToForPlan as wt, errorContractValidationFailed as x, resolveFromForPlan as xt, errorConsentPlanMismatch as y, resolveContractRef$1 as yt, executeDbInit as z };
|
|
4685
|
+
|
|
4686
|
+
//# sourceMappingURL=ref-h6InR6dO-rGfl18Jv.mjs.map
|